##// END OF EJS Templates
convert: rename sink hascommit to hascommitforsplicemap...
Mads Kiilerich -
r21634:23b24d6a default
parent child Browse files
Show More
@@ -1,443 +1,445 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
7
8 import base64, errno, subprocess, os, datetime, re
8 import base64, errno, subprocess, os, datetime, re
9 import cPickle as pickle
9 import cPickle as pickle
10 from mercurial import util
10 from mercurial import util
11 from mercurial.i18n import _
11 from mercurial.i18n import _
12
12
13 propertycache = util.propertycache
13 propertycache = util.propertycache
14
14
15 def encodeargs(args):
15 def encodeargs(args):
16 def encodearg(s):
16 def encodearg(s):
17 lines = base64.encodestring(s)
17 lines = base64.encodestring(s)
18 lines = [l.splitlines()[0] for l in lines]
18 lines = [l.splitlines()[0] for l in lines]
19 return ''.join(lines)
19 return ''.join(lines)
20
20
21 s = pickle.dumps(args)
21 s = pickle.dumps(args)
22 return encodearg(s)
22 return encodearg(s)
23
23
24 def decodeargs(s):
24 def decodeargs(s):
25 s = base64.decodestring(s)
25 s = base64.decodestring(s)
26 return pickle.loads(s)
26 return pickle.loads(s)
27
27
28 class MissingTool(Exception):
28 class MissingTool(Exception):
29 pass
29 pass
30
30
31 def checktool(exe, name=None, abort=True):
31 def checktool(exe, name=None, abort=True):
32 name = name or exe
32 name = name or exe
33 if not util.findexe(exe):
33 if not util.findexe(exe):
34 exc = abort and util.Abort or MissingTool
34 exc = abort and util.Abort or MissingTool
35 raise exc(_('cannot find required "%s" tool') % name)
35 raise exc(_('cannot find required "%s" tool') % name)
36
36
37 class NoRepo(Exception):
37 class NoRepo(Exception):
38 pass
38 pass
39
39
40 SKIPREV = 'SKIP'
40 SKIPREV = 'SKIP'
41
41
42 class commit(object):
42 class commit(object):
43 def __init__(self, author, date, desc, parents, branch=None, rev=None,
43 def __init__(self, author, date, desc, parents, branch=None, rev=None,
44 extra={}, sortkey=None):
44 extra={}, sortkey=None):
45 self.author = author or 'unknown'
45 self.author = author or 'unknown'
46 self.date = date or '0 0'
46 self.date = date or '0 0'
47 self.desc = desc
47 self.desc = desc
48 self.parents = parents
48 self.parents = parents
49 self.branch = branch
49 self.branch = branch
50 self.rev = rev
50 self.rev = rev
51 self.extra = extra
51 self.extra = extra
52 self.sortkey = sortkey
52 self.sortkey = sortkey
53
53
54 class converter_source(object):
54 class converter_source(object):
55 """Conversion source interface"""
55 """Conversion source interface"""
56
56
57 def __init__(self, ui, path=None, rev=None):
57 def __init__(self, ui, path=None, rev=None):
58 """Initialize conversion source (or raise NoRepo("message")
58 """Initialize conversion source (or raise NoRepo("message")
59 exception if path is not a valid repository)"""
59 exception if path is not a valid repository)"""
60 self.ui = ui
60 self.ui = ui
61 self.path = path
61 self.path = path
62 self.rev = rev
62 self.rev = rev
63
63
64 self.encoding = 'utf-8'
64 self.encoding = 'utf-8'
65
65
66 def checkhexformat(self, revstr, mapname='splicemap'):
66 def checkhexformat(self, revstr, mapname='splicemap'):
67 """ fails if revstr is not a 40 byte hex. mercurial and git both uses
67 """ fails if revstr is not a 40 byte hex. mercurial and git both uses
68 such format for their revision numbering
68 such format for their revision numbering
69 """
69 """
70 if not re.match(r'[0-9a-fA-F]{40,40}$', revstr):
70 if not re.match(r'[0-9a-fA-F]{40,40}$', revstr):
71 raise util.Abort(_('%s entry %s is not a valid revision'
71 raise util.Abort(_('%s entry %s is not a valid revision'
72 ' identifier') % (mapname, revstr))
72 ' identifier') % (mapname, revstr))
73
73
74 def before(self):
74 def before(self):
75 pass
75 pass
76
76
77 def after(self):
77 def after(self):
78 pass
78 pass
79
79
80 def setrevmap(self, revmap):
80 def setrevmap(self, revmap):
81 """set the map of already-converted revisions"""
81 """set the map of already-converted revisions"""
82 pass
82 pass
83
83
84 def getheads(self):
84 def getheads(self):
85 """Return a list of this repository's heads"""
85 """Return a list of this repository's heads"""
86 raise NotImplementedError
86 raise NotImplementedError
87
87
88 def getfile(self, name, rev):
88 def getfile(self, name, rev):
89 """Return a pair (data, mode) where data is the file content
89 """Return a pair (data, mode) where data is the file content
90 as a string and mode one of '', 'x' or 'l'. rev is the
90 as a string and mode one of '', 'x' or 'l'. rev is the
91 identifier returned by a previous call to getchanges(). Raise
91 identifier returned by a previous call to getchanges(). Raise
92 IOError to indicate that name was deleted in rev.
92 IOError to indicate that name was deleted in rev.
93 """
93 """
94 raise NotImplementedError
94 raise NotImplementedError
95
95
96 def getchanges(self, version):
96 def getchanges(self, version):
97 """Returns a tuple of (files, copies).
97 """Returns a tuple of (files, copies).
98
98
99 files is a sorted list of (filename, id) tuples for all files
99 files is a sorted list of (filename, id) tuples for all files
100 changed between version and its first parent returned by
100 changed between version and its first parent returned by
101 getcommit(). id is the source revision id of the file.
101 getcommit(). id is the source revision id of the file.
102
102
103 copies is a dictionary of dest: source
103 copies is a dictionary of dest: source
104 """
104 """
105 raise NotImplementedError
105 raise NotImplementedError
106
106
107 def getcommit(self, version):
107 def getcommit(self, version):
108 """Return the commit object for version"""
108 """Return the commit object for version"""
109 raise NotImplementedError
109 raise NotImplementedError
110
110
111 def gettags(self):
111 def gettags(self):
112 """Return the tags as a dictionary of name: revision
112 """Return the tags as a dictionary of name: revision
113
113
114 Tag names must be UTF-8 strings.
114 Tag names must be UTF-8 strings.
115 """
115 """
116 raise NotImplementedError
116 raise NotImplementedError
117
117
118 def recode(self, s, encoding=None):
118 def recode(self, s, encoding=None):
119 if not encoding:
119 if not encoding:
120 encoding = self.encoding or 'utf-8'
120 encoding = self.encoding or 'utf-8'
121
121
122 if isinstance(s, unicode):
122 if isinstance(s, unicode):
123 return s.encode("utf-8")
123 return s.encode("utf-8")
124 try:
124 try:
125 return s.decode(encoding).encode("utf-8")
125 return s.decode(encoding).encode("utf-8")
126 except UnicodeError:
126 except UnicodeError:
127 try:
127 try:
128 return s.decode("latin-1").encode("utf-8")
128 return s.decode("latin-1").encode("utf-8")
129 except UnicodeError:
129 except UnicodeError:
130 return s.decode(encoding, "replace").encode("utf-8")
130 return s.decode(encoding, "replace").encode("utf-8")
131
131
132 def getchangedfiles(self, rev, i):
132 def getchangedfiles(self, rev, i):
133 """Return the files changed by rev compared to parent[i].
133 """Return the files changed by rev compared to parent[i].
134
134
135 i is an index selecting one of the parents of rev. The return
135 i is an index selecting one of the parents of rev. The return
136 value should be the list of files that are different in rev and
136 value should be the list of files that are different in rev and
137 this parent.
137 this parent.
138
138
139 If rev has no parents, i is None.
139 If rev has no parents, i is None.
140
140
141 This function is only needed to support --filemap
141 This function is only needed to support --filemap
142 """
142 """
143 raise NotImplementedError
143 raise NotImplementedError
144
144
145 def converted(self, rev, sinkrev):
145 def converted(self, rev, sinkrev):
146 '''Notify the source that a revision has been converted.'''
146 '''Notify the source that a revision has been converted.'''
147 pass
147 pass
148
148
149 def hasnativeorder(self):
149 def hasnativeorder(self):
150 """Return true if this source has a meaningful, native revision
150 """Return true if this source has a meaningful, native revision
151 order. For instance, Mercurial revisions are store sequentially
151 order. For instance, Mercurial revisions are store sequentially
152 while there is no such global ordering with Darcs.
152 while there is no such global ordering with Darcs.
153 """
153 """
154 return False
154 return False
155
155
156 def hasnativeclose(self):
156 def hasnativeclose(self):
157 """Return true if this source has ability to close branch.
157 """Return true if this source has ability to close branch.
158 """
158 """
159 return False
159 return False
160
160
161 def lookuprev(self, rev):
161 def lookuprev(self, rev):
162 """If rev is a meaningful revision reference in source, return
162 """If rev is a meaningful revision reference in source, return
163 the referenced identifier in the same format used by getcommit().
163 the referenced identifier in the same format used by getcommit().
164 return None otherwise.
164 return None otherwise.
165 """
165 """
166 return None
166 return None
167
167
168 def getbookmarks(self):
168 def getbookmarks(self):
169 """Return the bookmarks as a dictionary of name: revision
169 """Return the bookmarks as a dictionary of name: revision
170
170
171 Bookmark names are to be UTF-8 strings.
171 Bookmark names are to be UTF-8 strings.
172 """
172 """
173 return {}
173 return {}
174
174
175 def checkrevformat(self, revstr, mapname='splicemap'):
175 def checkrevformat(self, revstr, mapname='splicemap'):
176 """revstr is a string that describes a revision in the given
176 """revstr is a string that describes a revision in the given
177 source control system. Return true if revstr has correct
177 source control system. Return true if revstr has correct
178 format.
178 format.
179 """
179 """
180 return True
180 return True
181
181
182 class converter_sink(object):
182 class converter_sink(object):
183 """Conversion sink (target) interface"""
183 """Conversion sink (target) interface"""
184
184
185 def __init__(self, ui, path):
185 def __init__(self, ui, path):
186 """Initialize conversion sink (or raise NoRepo("message")
186 """Initialize conversion sink (or raise NoRepo("message")
187 exception if path is not a valid repository)
187 exception if path is not a valid repository)
188
188
189 created is a list of paths to remove if a fatal error occurs
189 created is a list of paths to remove if a fatal error occurs
190 later"""
190 later"""
191 self.ui = ui
191 self.ui = ui
192 self.path = path
192 self.path = path
193 self.created = []
193 self.created = []
194
194
195 def revmapfile(self):
195 def revmapfile(self):
196 """Path to a file that will contain lines
196 """Path to a file that will contain lines
197 source_rev_id sink_rev_id
197 source_rev_id sink_rev_id
198 mapping equivalent revision identifiers for each system."""
198 mapping equivalent revision identifiers for each system."""
199 raise NotImplementedError
199 raise NotImplementedError
200
200
201 def authorfile(self):
201 def authorfile(self):
202 """Path to a file that will contain lines
202 """Path to a file that will contain lines
203 srcauthor=dstauthor
203 srcauthor=dstauthor
204 mapping equivalent authors identifiers for each system."""
204 mapping equivalent authors identifiers for each system."""
205 return None
205 return None
206
206
207 def putcommit(self, files, copies, parents, commit, source, revmap):
207 def putcommit(self, files, copies, parents, commit, source, revmap):
208 """Create a revision with all changed files listed in 'files'
208 """Create a revision with all changed files listed in 'files'
209 and having listed parents. 'commit' is a commit object
209 and having listed parents. 'commit' is a commit object
210 containing at a minimum the author, date, and message for this
210 containing at a minimum the author, date, and message for this
211 changeset. 'files' is a list of (path, version) tuples,
211 changeset. 'files' is a list of (path, version) tuples,
212 'copies' is a dictionary mapping destinations to sources,
212 'copies' is a dictionary mapping destinations to sources,
213 'source' is the source repository, and 'revmap' is a mapfile
213 'source' is the source repository, and 'revmap' is a mapfile
214 of source revisions to converted revisions. Only getfile() and
214 of source revisions to converted revisions. Only getfile() and
215 lookuprev() should be called on 'source'.
215 lookuprev() should be called on 'source'.
216
216
217 Note that the sink repository is not told to update itself to
217 Note that the sink repository is not told to update itself to
218 a particular revision (or even what that revision would be)
218 a particular revision (or even what that revision would be)
219 before it receives the file data.
219 before it receives the file data.
220 """
220 """
221 raise NotImplementedError
221 raise NotImplementedError
222
222
223 def puttags(self, tags):
223 def puttags(self, tags):
224 """Put tags into sink.
224 """Put tags into sink.
225
225
226 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
226 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
227 Return a pair (tag_revision, tag_parent_revision), or (None, None)
227 Return a pair (tag_revision, tag_parent_revision), or (None, None)
228 if nothing was changed.
228 if nothing was changed.
229 """
229 """
230 raise NotImplementedError
230 raise NotImplementedError
231
231
232 def setbranch(self, branch, pbranches):
232 def setbranch(self, branch, pbranches):
233 """Set the current branch name. Called before the first putcommit
233 """Set the current branch name. Called before the first putcommit
234 on the branch.
234 on the branch.
235 branch: branch name for subsequent commits
235 branch: branch name for subsequent commits
236 pbranches: (converted parent revision, parent branch) tuples"""
236 pbranches: (converted parent revision, parent branch) tuples"""
237 pass
237 pass
238
238
239 def setfilemapmode(self, active):
239 def setfilemapmode(self, active):
240 """Tell the destination that we're using a filemap
240 """Tell the destination that we're using a filemap
241
241
242 Some converter_sources (svn in particular) can claim that a file
242 Some converter_sources (svn in particular) can claim that a file
243 was changed in a revision, even if there was no change. This method
243 was changed in a revision, even if there was no change. This method
244 tells the destination that we're using a filemap and that it should
244 tells the destination that we're using a filemap and that it should
245 filter empty revisions.
245 filter empty revisions.
246 """
246 """
247 pass
247 pass
248
248
249 def before(self):
249 def before(self):
250 pass
250 pass
251
251
252 def after(self):
252 def after(self):
253 pass
253 pass
254
254
255 def putbookmarks(self, bookmarks):
255 def putbookmarks(self, bookmarks):
256 """Put bookmarks into sink.
256 """Put bookmarks into sink.
257
257
258 bookmarks: {bookmarkname: sink_rev_id, ...}
258 bookmarks: {bookmarkname: sink_rev_id, ...}
259 where bookmarkname is an UTF-8 string.
259 where bookmarkname is an UTF-8 string.
260 """
260 """
261 pass
261 pass
262
262
263 def hascommit(self, rev):
263 def hascommitforsplicemap(self, rev):
264 """Return True if the sink contains rev"""
264 """This method is for the special needs for splicemap handling and not
265 for general use. Returns True if the sink contains rev, aborts on some
266 special cases."""
265 raise NotImplementedError
267 raise NotImplementedError
266
268
267 class commandline(object):
269 class commandline(object):
268 def __init__(self, ui, command):
270 def __init__(self, ui, command):
269 self.ui = ui
271 self.ui = ui
270 self.command = command
272 self.command = command
271
273
272 def prerun(self):
274 def prerun(self):
273 pass
275 pass
274
276
275 def postrun(self):
277 def postrun(self):
276 pass
278 pass
277
279
278 def _cmdline(self, cmd, *args, **kwargs):
280 def _cmdline(self, cmd, *args, **kwargs):
279 cmdline = [self.command, cmd] + list(args)
281 cmdline = [self.command, cmd] + list(args)
280 for k, v in kwargs.iteritems():
282 for k, v in kwargs.iteritems():
281 if len(k) == 1:
283 if len(k) == 1:
282 cmdline.append('-' + k)
284 cmdline.append('-' + k)
283 else:
285 else:
284 cmdline.append('--' + k.replace('_', '-'))
286 cmdline.append('--' + k.replace('_', '-'))
285 try:
287 try:
286 if len(k) == 1:
288 if len(k) == 1:
287 cmdline.append('' + v)
289 cmdline.append('' + v)
288 else:
290 else:
289 cmdline[-1] += '=' + v
291 cmdline[-1] += '=' + v
290 except TypeError:
292 except TypeError:
291 pass
293 pass
292 cmdline = [util.shellquote(arg) for arg in cmdline]
294 cmdline = [util.shellquote(arg) for arg in cmdline]
293 if not self.ui.debugflag:
295 if not self.ui.debugflag:
294 cmdline += ['2>', os.devnull]
296 cmdline += ['2>', os.devnull]
295 cmdline = ' '.join(cmdline)
297 cmdline = ' '.join(cmdline)
296 return cmdline
298 return cmdline
297
299
298 def _run(self, cmd, *args, **kwargs):
300 def _run(self, cmd, *args, **kwargs):
299 def popen(cmdline):
301 def popen(cmdline):
300 p = subprocess.Popen(cmdline, shell=True, bufsize=-1,
302 p = subprocess.Popen(cmdline, shell=True, bufsize=-1,
301 close_fds=util.closefds,
303 close_fds=util.closefds,
302 stdout=subprocess.PIPE)
304 stdout=subprocess.PIPE)
303 return p
305 return p
304 return self._dorun(popen, cmd, *args, **kwargs)
306 return self._dorun(popen, cmd, *args, **kwargs)
305
307
306 def _run2(self, cmd, *args, **kwargs):
308 def _run2(self, cmd, *args, **kwargs):
307 return self._dorun(util.popen2, cmd, *args, **kwargs)
309 return self._dorun(util.popen2, cmd, *args, **kwargs)
308
310
309 def _dorun(self, openfunc, cmd, *args, **kwargs):
311 def _dorun(self, openfunc, cmd, *args, **kwargs):
310 cmdline = self._cmdline(cmd, *args, **kwargs)
312 cmdline = self._cmdline(cmd, *args, **kwargs)
311 self.ui.debug('running: %s\n' % (cmdline,))
313 self.ui.debug('running: %s\n' % (cmdline,))
312 self.prerun()
314 self.prerun()
313 try:
315 try:
314 return openfunc(cmdline)
316 return openfunc(cmdline)
315 finally:
317 finally:
316 self.postrun()
318 self.postrun()
317
319
318 def run(self, cmd, *args, **kwargs):
320 def run(self, cmd, *args, **kwargs):
319 p = self._run(cmd, *args, **kwargs)
321 p = self._run(cmd, *args, **kwargs)
320 output = p.communicate()[0]
322 output = p.communicate()[0]
321 self.ui.debug(output)
323 self.ui.debug(output)
322 return output, p.returncode
324 return output, p.returncode
323
325
324 def runlines(self, cmd, *args, **kwargs):
326 def runlines(self, cmd, *args, **kwargs):
325 p = self._run(cmd, *args, **kwargs)
327 p = self._run(cmd, *args, **kwargs)
326 output = p.stdout.readlines()
328 output = p.stdout.readlines()
327 p.wait()
329 p.wait()
328 self.ui.debug(''.join(output))
330 self.ui.debug(''.join(output))
329 return output, p.returncode
331 return output, p.returncode
330
332
331 def checkexit(self, status, output=''):
333 def checkexit(self, status, output=''):
332 if status:
334 if status:
333 if output:
335 if output:
334 self.ui.warn(_('%s error:\n') % self.command)
336 self.ui.warn(_('%s error:\n') % self.command)
335 self.ui.warn(output)
337 self.ui.warn(output)
336 msg = util.explainexit(status)[0]
338 msg = util.explainexit(status)[0]
337 raise util.Abort('%s %s' % (self.command, msg))
339 raise util.Abort('%s %s' % (self.command, msg))
338
340
339 def run0(self, cmd, *args, **kwargs):
341 def run0(self, cmd, *args, **kwargs):
340 output, status = self.run(cmd, *args, **kwargs)
342 output, status = self.run(cmd, *args, **kwargs)
341 self.checkexit(status, output)
343 self.checkexit(status, output)
342 return output
344 return output
343
345
344 def runlines0(self, cmd, *args, **kwargs):
346 def runlines0(self, cmd, *args, **kwargs):
345 output, status = self.runlines(cmd, *args, **kwargs)
347 output, status = self.runlines(cmd, *args, **kwargs)
346 self.checkexit(status, ''.join(output))
348 self.checkexit(status, ''.join(output))
347 return output
349 return output
348
350
349 @propertycache
351 @propertycache
350 def argmax(self):
352 def argmax(self):
351 # POSIX requires at least 4096 bytes for ARG_MAX
353 # POSIX requires at least 4096 bytes for ARG_MAX
352 argmax = 4096
354 argmax = 4096
353 try:
355 try:
354 argmax = os.sysconf("SC_ARG_MAX")
356 argmax = os.sysconf("SC_ARG_MAX")
355 except (AttributeError, ValueError):
357 except (AttributeError, ValueError):
356 pass
358 pass
357
359
358 # Windows shells impose their own limits on command line length,
360 # Windows shells impose their own limits on command line length,
359 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
361 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
360 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
362 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
361 # details about cmd.exe limitations.
363 # details about cmd.exe limitations.
362
364
363 # Since ARG_MAX is for command line _and_ environment, lower our limit
365 # Since ARG_MAX is for command line _and_ environment, lower our limit
364 # (and make happy Windows shells while doing this).
366 # (and make happy Windows shells while doing this).
365 return argmax // 2 - 1
367 return argmax // 2 - 1
366
368
367 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
369 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
368 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
370 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
369 limit = self.argmax - cmdlen
371 limit = self.argmax - cmdlen
370 bytes = 0
372 bytes = 0
371 fl = []
373 fl = []
372 for fn in arglist:
374 for fn in arglist:
373 b = len(fn) + 3
375 b = len(fn) + 3
374 if bytes + b < limit or len(fl) == 0:
376 if bytes + b < limit or len(fl) == 0:
375 fl.append(fn)
377 fl.append(fn)
376 bytes += b
378 bytes += b
377 else:
379 else:
378 yield fl
380 yield fl
379 fl = [fn]
381 fl = [fn]
380 bytes = b
382 bytes = b
381 if fl:
383 if fl:
382 yield fl
384 yield fl
383
385
384 def xargs(self, arglist, cmd, *args, **kwargs):
386 def xargs(self, arglist, cmd, *args, **kwargs):
385 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
387 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
386 self.run0(cmd, *(list(args) + l), **kwargs)
388 self.run0(cmd, *(list(args) + l), **kwargs)
387
389
388 class mapfile(dict):
390 class mapfile(dict):
389 def __init__(self, ui, path):
391 def __init__(self, ui, path):
390 super(mapfile, self).__init__()
392 super(mapfile, self).__init__()
391 self.ui = ui
393 self.ui = ui
392 self.path = path
394 self.path = path
393 self.fp = None
395 self.fp = None
394 self.order = []
396 self.order = []
395 self._read()
397 self._read()
396
398
397 def _read(self):
399 def _read(self):
398 if not self.path:
400 if not self.path:
399 return
401 return
400 try:
402 try:
401 fp = open(self.path, 'r')
403 fp = open(self.path, 'r')
402 except IOError, err:
404 except IOError, err:
403 if err.errno != errno.ENOENT:
405 if err.errno != errno.ENOENT:
404 raise
406 raise
405 return
407 return
406 for i, line in enumerate(fp):
408 for i, line in enumerate(fp):
407 line = line.splitlines()[0].rstrip()
409 line = line.splitlines()[0].rstrip()
408 if not line:
410 if not line:
409 # Ignore blank lines
411 # Ignore blank lines
410 continue
412 continue
411 try:
413 try:
412 key, value = line.rsplit(' ', 1)
414 key, value = line.rsplit(' ', 1)
413 except ValueError:
415 except ValueError:
414 raise util.Abort(
416 raise util.Abort(
415 _('syntax error in %s(%d): key/value pair expected')
417 _('syntax error in %s(%d): key/value pair expected')
416 % (self.path, i + 1))
418 % (self.path, i + 1))
417 if key not in self:
419 if key not in self:
418 self.order.append(key)
420 self.order.append(key)
419 super(mapfile, self).__setitem__(key, value)
421 super(mapfile, self).__setitem__(key, value)
420 fp.close()
422 fp.close()
421
423
422 def __setitem__(self, key, value):
424 def __setitem__(self, key, value):
423 if self.fp is None:
425 if self.fp is None:
424 try:
426 try:
425 self.fp = open(self.path, 'a')
427 self.fp = open(self.path, 'a')
426 except IOError, err:
428 except IOError, err:
427 raise util.Abort(_('could not open map file %r: %s') %
429 raise util.Abort(_('could not open map file %r: %s') %
428 (self.path, err.strerror))
430 (self.path, err.strerror))
429 self.fp.write('%s %s\n' % (key, value))
431 self.fp.write('%s %s\n' % (key, value))
430 self.fp.flush()
432 self.fp.flush()
431 super(mapfile, self).__setitem__(key, value)
433 super(mapfile, self).__setitem__(key, value)
432
434
433 def close(self):
435 def close(self):
434 if self.fp:
436 if self.fp:
435 self.fp.close()
437 self.fp.close()
436 self.fp = None
438 self.fp = None
437
439
438 def makedatetimestamp(t):
440 def makedatetimestamp(t):
439 """Like util.makedate() but for time t instead of current time"""
441 """Like util.makedate() but for time t instead of current time"""
440 delta = (datetime.datetime.utcfromtimestamp(t) -
442 delta = (datetime.datetime.utcfromtimestamp(t) -
441 datetime.datetime.fromtimestamp(t))
443 datetime.datetime.fromtimestamp(t))
442 tz = delta.days * 86400 + delta.seconds
444 tz = delta.days * 86400 + delta.seconds
443 return t, tz
445 return t, tz
@@ -1,528 +1,528 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
7
8 from common import NoRepo, MissingTool, SKIPREV, mapfile
8 from common import NoRepo, MissingTool, SKIPREV, mapfile
9 from cvs import convert_cvs
9 from cvs import convert_cvs
10 from darcs import darcs_source
10 from darcs import darcs_source
11 from git import convert_git
11 from git import convert_git
12 from hg import mercurial_source, mercurial_sink
12 from hg import mercurial_source, mercurial_sink
13 from subversion import svn_source, svn_sink
13 from subversion import svn_source, svn_sink
14 from monotone import monotone_source
14 from monotone import monotone_source
15 from gnuarch import gnuarch_source
15 from gnuarch import gnuarch_source
16 from bzr import bzr_source
16 from bzr import bzr_source
17 from p4 import p4_source
17 from p4 import p4_source
18 import filemap
18 import filemap
19
19
20 import os, shutil, shlex
20 import os, shutil, shlex
21 from mercurial import hg, util, encoding
21 from mercurial import hg, util, encoding
22 from mercurial.i18n import _
22 from mercurial.i18n import _
23
23
24 orig_encoding = 'ascii'
24 orig_encoding = 'ascii'
25
25
26 def recode(s):
26 def recode(s):
27 if isinstance(s, unicode):
27 if isinstance(s, unicode):
28 return s.encode(orig_encoding, 'replace')
28 return s.encode(orig_encoding, 'replace')
29 else:
29 else:
30 return s.decode('utf-8').encode(orig_encoding, 'replace')
30 return s.decode('utf-8').encode(orig_encoding, 'replace')
31
31
32 source_converters = [
32 source_converters = [
33 ('cvs', convert_cvs, 'branchsort'),
33 ('cvs', convert_cvs, 'branchsort'),
34 ('git', convert_git, 'branchsort'),
34 ('git', convert_git, 'branchsort'),
35 ('svn', svn_source, 'branchsort'),
35 ('svn', svn_source, 'branchsort'),
36 ('hg', mercurial_source, 'sourcesort'),
36 ('hg', mercurial_source, 'sourcesort'),
37 ('darcs', darcs_source, 'branchsort'),
37 ('darcs', darcs_source, 'branchsort'),
38 ('mtn', monotone_source, 'branchsort'),
38 ('mtn', monotone_source, 'branchsort'),
39 ('gnuarch', gnuarch_source, 'branchsort'),
39 ('gnuarch', gnuarch_source, 'branchsort'),
40 ('bzr', bzr_source, 'branchsort'),
40 ('bzr', bzr_source, 'branchsort'),
41 ('p4', p4_source, 'branchsort'),
41 ('p4', p4_source, 'branchsort'),
42 ]
42 ]
43
43
44 sink_converters = [
44 sink_converters = [
45 ('hg', mercurial_sink),
45 ('hg', mercurial_sink),
46 ('svn', svn_sink),
46 ('svn', svn_sink),
47 ]
47 ]
48
48
49 def convertsource(ui, path, type, rev):
49 def convertsource(ui, path, type, rev):
50 exceptions = []
50 exceptions = []
51 if type and type not in [s[0] for s in source_converters]:
51 if type and type not in [s[0] for s in source_converters]:
52 raise util.Abort(_('%s: invalid source repository type') % type)
52 raise util.Abort(_('%s: invalid source repository type') % type)
53 for name, source, sortmode in source_converters:
53 for name, source, sortmode in source_converters:
54 try:
54 try:
55 if not type or name == type:
55 if not type or name == type:
56 return source(ui, path, rev), sortmode
56 return source(ui, path, rev), sortmode
57 except (NoRepo, MissingTool), inst:
57 except (NoRepo, MissingTool), inst:
58 exceptions.append(inst)
58 exceptions.append(inst)
59 if not ui.quiet:
59 if not ui.quiet:
60 for inst in exceptions:
60 for inst in exceptions:
61 ui.write("%s\n" % inst)
61 ui.write("%s\n" % inst)
62 raise util.Abort(_('%s: missing or unsupported repository') % path)
62 raise util.Abort(_('%s: missing or unsupported repository') % path)
63
63
64 def convertsink(ui, path, type):
64 def convertsink(ui, path, type):
65 if type and type not in [s[0] for s in sink_converters]:
65 if type and type not in [s[0] for s in sink_converters]:
66 raise util.Abort(_('%s: invalid destination repository type') % type)
66 raise util.Abort(_('%s: invalid destination repository type') % type)
67 for name, sink in sink_converters:
67 for name, sink in sink_converters:
68 try:
68 try:
69 if not type or name == type:
69 if not type or name == type:
70 return sink(ui, path)
70 return sink(ui, path)
71 except NoRepo, inst:
71 except NoRepo, inst:
72 ui.note(_("convert: %s\n") % inst)
72 ui.note(_("convert: %s\n") % inst)
73 except MissingTool, inst:
73 except MissingTool, inst:
74 raise util.Abort('%s\n' % inst)
74 raise util.Abort('%s\n' % inst)
75 raise util.Abort(_('%s: unknown repository type') % path)
75 raise util.Abort(_('%s: unknown repository type') % path)
76
76
77 class progresssource(object):
77 class progresssource(object):
78 def __init__(self, ui, source, filecount):
78 def __init__(self, ui, source, filecount):
79 self.ui = ui
79 self.ui = ui
80 self.source = source
80 self.source = source
81 self.filecount = filecount
81 self.filecount = filecount
82 self.retrieved = 0
82 self.retrieved = 0
83
83
84 def getfile(self, file, rev):
84 def getfile(self, file, rev):
85 self.retrieved += 1
85 self.retrieved += 1
86 self.ui.progress(_('getting files'), self.retrieved,
86 self.ui.progress(_('getting files'), self.retrieved,
87 item=file, total=self.filecount)
87 item=file, total=self.filecount)
88 return self.source.getfile(file, rev)
88 return self.source.getfile(file, rev)
89
89
90 def lookuprev(self, rev):
90 def lookuprev(self, rev):
91 return self.source.lookuprev(rev)
91 return self.source.lookuprev(rev)
92
92
93 def close(self):
93 def close(self):
94 self.ui.progress(_('getting files'), None)
94 self.ui.progress(_('getting files'), None)
95
95
96 class converter(object):
96 class converter(object):
97 def __init__(self, ui, source, dest, revmapfile, opts):
97 def __init__(self, ui, source, dest, revmapfile, opts):
98
98
99 self.source = source
99 self.source = source
100 self.dest = dest
100 self.dest = dest
101 self.ui = ui
101 self.ui = ui
102 self.opts = opts
102 self.opts = opts
103 self.commitcache = {}
103 self.commitcache = {}
104 self.authors = {}
104 self.authors = {}
105 self.authorfile = None
105 self.authorfile = None
106
106
107 # Record converted revisions persistently: maps source revision
107 # Record converted revisions persistently: maps source revision
108 # ID to target revision ID (both strings). (This is how
108 # ID to target revision ID (both strings). (This is how
109 # incremental conversions work.)
109 # incremental conversions work.)
110 self.map = mapfile(ui, revmapfile)
110 self.map = mapfile(ui, revmapfile)
111
111
112 # Read first the dst author map if any
112 # Read first the dst author map if any
113 authorfile = self.dest.authorfile()
113 authorfile = self.dest.authorfile()
114 if authorfile and os.path.exists(authorfile):
114 if authorfile and os.path.exists(authorfile):
115 self.readauthormap(authorfile)
115 self.readauthormap(authorfile)
116 # Extend/Override with new author map if necessary
116 # Extend/Override with new author map if necessary
117 if opts.get('authormap'):
117 if opts.get('authormap'):
118 self.readauthormap(opts.get('authormap'))
118 self.readauthormap(opts.get('authormap'))
119 self.authorfile = self.dest.authorfile()
119 self.authorfile = self.dest.authorfile()
120
120
121 self.splicemap = self.parsesplicemap(opts.get('splicemap'))
121 self.splicemap = self.parsesplicemap(opts.get('splicemap'))
122 self.branchmap = mapfile(ui, opts.get('branchmap'))
122 self.branchmap = mapfile(ui, opts.get('branchmap'))
123
123
124 def parsesplicemap(self, path):
124 def parsesplicemap(self, path):
125 """ check and validate the splicemap format and
125 """ check and validate the splicemap format and
126 return a child/parents dictionary.
126 return a child/parents dictionary.
127 Format checking has two parts.
127 Format checking has two parts.
128 1. generic format which is same across all source types
128 1. generic format which is same across all source types
129 2. specific format checking which may be different for
129 2. specific format checking which may be different for
130 different source type. This logic is implemented in
130 different source type. This logic is implemented in
131 checkrevformat function in source files like
131 checkrevformat function in source files like
132 hg.py, subversion.py etc.
132 hg.py, subversion.py etc.
133 """
133 """
134
134
135 if not path:
135 if not path:
136 return {}
136 return {}
137 m = {}
137 m = {}
138 try:
138 try:
139 fp = open(path, 'r')
139 fp = open(path, 'r')
140 for i, line in enumerate(fp):
140 for i, line in enumerate(fp):
141 line = line.splitlines()[0].rstrip()
141 line = line.splitlines()[0].rstrip()
142 if not line:
142 if not line:
143 # Ignore blank lines
143 # Ignore blank lines
144 continue
144 continue
145 # split line
145 # split line
146 lex = shlex.shlex(line, posix=True)
146 lex = shlex.shlex(line, posix=True)
147 lex.whitespace_split = True
147 lex.whitespace_split = True
148 lex.whitespace += ','
148 lex.whitespace += ','
149 line = list(lex)
149 line = list(lex)
150 # check number of parents
150 # check number of parents
151 if not (2 <= len(line) <= 3):
151 if not (2 <= len(line) <= 3):
152 raise util.Abort(_('syntax error in %s(%d): child parent1'
152 raise util.Abort(_('syntax error in %s(%d): child parent1'
153 '[,parent2] expected') % (path, i + 1))
153 '[,parent2] expected') % (path, i + 1))
154 for part in line:
154 for part in line:
155 self.source.checkrevformat(part)
155 self.source.checkrevformat(part)
156 child, p1, p2 = line[0], line[1:2], line[2:]
156 child, p1, p2 = line[0], line[1:2], line[2:]
157 if p1 == p2:
157 if p1 == p2:
158 m[child] = p1
158 m[child] = p1
159 else:
159 else:
160 m[child] = p1 + p2
160 m[child] = p1 + p2
161 # if file does not exist or error reading, exit
161 # if file does not exist or error reading, exit
162 except IOError:
162 except IOError:
163 raise util.Abort(_('splicemap file not found or error reading %s:')
163 raise util.Abort(_('splicemap file not found or error reading %s:')
164 % path)
164 % path)
165 return m
165 return m
166
166
167
167
168 def walktree(self, heads):
168 def walktree(self, heads):
169 '''Return a mapping that identifies the uncommitted parents of every
169 '''Return a mapping that identifies the uncommitted parents of every
170 uncommitted changeset.'''
170 uncommitted changeset.'''
171 visit = heads
171 visit = heads
172 known = set()
172 known = set()
173 parents = {}
173 parents = {}
174 while visit:
174 while visit:
175 n = visit.pop(0)
175 n = visit.pop(0)
176 if n in known or n in self.map:
176 if n in known or n in self.map:
177 continue
177 continue
178 known.add(n)
178 known.add(n)
179 self.ui.progress(_('scanning'), len(known), unit=_('revisions'))
179 self.ui.progress(_('scanning'), len(known), unit=_('revisions'))
180 commit = self.cachecommit(n)
180 commit = self.cachecommit(n)
181 parents[n] = []
181 parents[n] = []
182 for p in commit.parents:
182 for p in commit.parents:
183 parents[n].append(p)
183 parents[n].append(p)
184 visit.append(p)
184 visit.append(p)
185 self.ui.progress(_('scanning'), None)
185 self.ui.progress(_('scanning'), None)
186
186
187 return parents
187 return parents
188
188
189 def mergesplicemap(self, parents, splicemap):
189 def mergesplicemap(self, parents, splicemap):
190 """A splicemap redefines child/parent relationships. Check the
190 """A splicemap redefines child/parent relationships. Check the
191 map contains valid revision identifiers and merge the new
191 map contains valid revision identifiers and merge the new
192 links in the source graph.
192 links in the source graph.
193 """
193 """
194 for c in sorted(splicemap):
194 for c in sorted(splicemap):
195 if c not in parents:
195 if c not in parents:
196 if not self.dest.hascommit(self.map.get(c, c)):
196 if not self.dest.hascommitforsplicemap(self.map.get(c, c)):
197 # Could be in source but not converted during this run
197 # Could be in source but not converted during this run
198 self.ui.warn(_('splice map revision %s is not being '
198 self.ui.warn(_('splice map revision %s is not being '
199 'converted, ignoring\n') % c)
199 'converted, ignoring\n') % c)
200 continue
200 continue
201 pc = []
201 pc = []
202 for p in splicemap[c]:
202 for p in splicemap[c]:
203 # We do not have to wait for nodes already in dest.
203 # We do not have to wait for nodes already in dest.
204 if self.dest.hascommit(self.map.get(p, p)):
204 if self.dest.hascommitforsplicemap(self.map.get(p, p)):
205 continue
205 continue
206 # Parent is not in dest and not being converted, not good
206 # Parent is not in dest and not being converted, not good
207 if p not in parents:
207 if p not in parents:
208 raise util.Abort(_('unknown splice map parent: %s') % p)
208 raise util.Abort(_('unknown splice map parent: %s') % p)
209 pc.append(p)
209 pc.append(p)
210 parents[c] = pc
210 parents[c] = pc
211
211
212 def toposort(self, parents, sortmode):
212 def toposort(self, parents, sortmode):
213 '''Return an ordering such that every uncommitted changeset is
213 '''Return an ordering such that every uncommitted changeset is
214 preceded by all its uncommitted ancestors.'''
214 preceded by all its uncommitted ancestors.'''
215
215
216 def mapchildren(parents):
216 def mapchildren(parents):
217 """Return a (children, roots) tuple where 'children' maps parent
217 """Return a (children, roots) tuple where 'children' maps parent
218 revision identifiers to children ones, and 'roots' is the list of
218 revision identifiers to children ones, and 'roots' is the list of
219 revisions without parents. 'parents' must be a mapping of revision
219 revisions without parents. 'parents' must be a mapping of revision
220 identifier to its parents ones.
220 identifier to its parents ones.
221 """
221 """
222 visit = sorted(parents)
222 visit = sorted(parents)
223 seen = set()
223 seen = set()
224 children = {}
224 children = {}
225 roots = []
225 roots = []
226
226
227 while visit:
227 while visit:
228 n = visit.pop(0)
228 n = visit.pop(0)
229 if n in seen:
229 if n in seen:
230 continue
230 continue
231 seen.add(n)
231 seen.add(n)
232 # Ensure that nodes without parents are present in the
232 # Ensure that nodes without parents are present in the
233 # 'children' mapping.
233 # 'children' mapping.
234 children.setdefault(n, [])
234 children.setdefault(n, [])
235 hasparent = False
235 hasparent = False
236 for p in parents[n]:
236 for p in parents[n]:
237 if p not in self.map:
237 if p not in self.map:
238 visit.append(p)
238 visit.append(p)
239 hasparent = True
239 hasparent = True
240 children.setdefault(p, []).append(n)
240 children.setdefault(p, []).append(n)
241 if not hasparent:
241 if not hasparent:
242 roots.append(n)
242 roots.append(n)
243
243
244 return children, roots
244 return children, roots
245
245
246 # Sort functions are supposed to take a list of revisions which
246 # Sort functions are supposed to take a list of revisions which
247 # can be converted immediately and pick one
247 # can be converted immediately and pick one
248
248
249 def makebranchsorter():
249 def makebranchsorter():
250 """If the previously converted revision has a child in the
250 """If the previously converted revision has a child in the
251 eligible revisions list, pick it. Return the list head
251 eligible revisions list, pick it. Return the list head
252 otherwise. Branch sort attempts to minimize branch
252 otherwise. Branch sort attempts to minimize branch
253 switching, which is harmful for Mercurial backend
253 switching, which is harmful for Mercurial backend
254 compression.
254 compression.
255 """
255 """
256 prev = [None]
256 prev = [None]
257 def picknext(nodes):
257 def picknext(nodes):
258 next = nodes[0]
258 next = nodes[0]
259 for n in nodes:
259 for n in nodes:
260 if prev[0] in parents[n]:
260 if prev[0] in parents[n]:
261 next = n
261 next = n
262 break
262 break
263 prev[0] = next
263 prev[0] = next
264 return next
264 return next
265 return picknext
265 return picknext
266
266
267 def makesourcesorter():
267 def makesourcesorter():
268 """Source specific sort."""
268 """Source specific sort."""
269 keyfn = lambda n: self.commitcache[n].sortkey
269 keyfn = lambda n: self.commitcache[n].sortkey
270 def picknext(nodes):
270 def picknext(nodes):
271 return sorted(nodes, key=keyfn)[0]
271 return sorted(nodes, key=keyfn)[0]
272 return picknext
272 return picknext
273
273
274 def makeclosesorter():
274 def makeclosesorter():
275 """Close order sort."""
275 """Close order sort."""
276 keyfn = lambda n: ('close' not in self.commitcache[n].extra,
276 keyfn = lambda n: ('close' not in self.commitcache[n].extra,
277 self.commitcache[n].sortkey)
277 self.commitcache[n].sortkey)
278 def picknext(nodes):
278 def picknext(nodes):
279 return sorted(nodes, key=keyfn)[0]
279 return sorted(nodes, key=keyfn)[0]
280 return picknext
280 return picknext
281
281
282 def makedatesorter():
282 def makedatesorter():
283 """Sort revisions by date."""
283 """Sort revisions by date."""
284 dates = {}
284 dates = {}
285 def getdate(n):
285 def getdate(n):
286 if n not in dates:
286 if n not in dates:
287 dates[n] = util.parsedate(self.commitcache[n].date)
287 dates[n] = util.parsedate(self.commitcache[n].date)
288 return dates[n]
288 return dates[n]
289
289
290 def picknext(nodes):
290 def picknext(nodes):
291 return min([(getdate(n), n) for n in nodes])[1]
291 return min([(getdate(n), n) for n in nodes])[1]
292
292
293 return picknext
293 return picknext
294
294
295 if sortmode == 'branchsort':
295 if sortmode == 'branchsort':
296 picknext = makebranchsorter()
296 picknext = makebranchsorter()
297 elif sortmode == 'datesort':
297 elif sortmode == 'datesort':
298 picknext = makedatesorter()
298 picknext = makedatesorter()
299 elif sortmode == 'sourcesort':
299 elif sortmode == 'sourcesort':
300 picknext = makesourcesorter()
300 picknext = makesourcesorter()
301 elif sortmode == 'closesort':
301 elif sortmode == 'closesort':
302 picknext = makeclosesorter()
302 picknext = makeclosesorter()
303 else:
303 else:
304 raise util.Abort(_('unknown sort mode: %s') % sortmode)
304 raise util.Abort(_('unknown sort mode: %s') % sortmode)
305
305
306 children, actives = mapchildren(parents)
306 children, actives = mapchildren(parents)
307
307
308 s = []
308 s = []
309 pendings = {}
309 pendings = {}
310 while actives:
310 while actives:
311 n = picknext(actives)
311 n = picknext(actives)
312 actives.remove(n)
312 actives.remove(n)
313 s.append(n)
313 s.append(n)
314
314
315 # Update dependents list
315 # Update dependents list
316 for c in children.get(n, []):
316 for c in children.get(n, []):
317 if c not in pendings:
317 if c not in pendings:
318 pendings[c] = [p for p in parents[c] if p not in self.map]
318 pendings[c] = [p for p in parents[c] if p not in self.map]
319 try:
319 try:
320 pendings[c].remove(n)
320 pendings[c].remove(n)
321 except ValueError:
321 except ValueError:
322 raise util.Abort(_('cycle detected between %s and %s')
322 raise util.Abort(_('cycle detected between %s and %s')
323 % (recode(c), recode(n)))
323 % (recode(c), recode(n)))
324 if not pendings[c]:
324 if not pendings[c]:
325 # Parents are converted, node is eligible
325 # Parents are converted, node is eligible
326 actives.insert(0, c)
326 actives.insert(0, c)
327 pendings[c] = None
327 pendings[c] = None
328
328
329 if len(s) != len(parents):
329 if len(s) != len(parents):
330 raise util.Abort(_("not all revisions were sorted"))
330 raise util.Abort(_("not all revisions were sorted"))
331
331
332 return s
332 return s
333
333
334 def writeauthormap(self):
334 def writeauthormap(self):
335 authorfile = self.authorfile
335 authorfile = self.authorfile
336 if authorfile:
336 if authorfile:
337 self.ui.status(_('writing author map file %s\n') % authorfile)
337 self.ui.status(_('writing author map file %s\n') % authorfile)
338 ofile = open(authorfile, 'w+')
338 ofile = open(authorfile, 'w+')
339 for author in self.authors:
339 for author in self.authors:
340 ofile.write("%s=%s\n" % (author, self.authors[author]))
340 ofile.write("%s=%s\n" % (author, self.authors[author]))
341 ofile.close()
341 ofile.close()
342
342
343 def readauthormap(self, authorfile):
343 def readauthormap(self, authorfile):
344 afile = open(authorfile, 'r')
344 afile = open(authorfile, 'r')
345 for line in afile:
345 for line in afile:
346
346
347 line = line.strip()
347 line = line.strip()
348 if not line or line.startswith('#'):
348 if not line or line.startswith('#'):
349 continue
349 continue
350
350
351 try:
351 try:
352 srcauthor, dstauthor = line.split('=', 1)
352 srcauthor, dstauthor = line.split('=', 1)
353 except ValueError:
353 except ValueError:
354 msg = _('ignoring bad line in author map file %s: %s\n')
354 msg = _('ignoring bad line in author map file %s: %s\n')
355 self.ui.warn(msg % (authorfile, line.rstrip()))
355 self.ui.warn(msg % (authorfile, line.rstrip()))
356 continue
356 continue
357
357
358 srcauthor = srcauthor.strip()
358 srcauthor = srcauthor.strip()
359 dstauthor = dstauthor.strip()
359 dstauthor = dstauthor.strip()
360 if self.authors.get(srcauthor) in (None, dstauthor):
360 if self.authors.get(srcauthor) in (None, dstauthor):
361 msg = _('mapping author %s to %s\n')
361 msg = _('mapping author %s to %s\n')
362 self.ui.debug(msg % (srcauthor, dstauthor))
362 self.ui.debug(msg % (srcauthor, dstauthor))
363 self.authors[srcauthor] = dstauthor
363 self.authors[srcauthor] = dstauthor
364 continue
364 continue
365
365
366 m = _('overriding mapping for author %s, was %s, will be %s\n')
366 m = _('overriding mapping for author %s, was %s, will be %s\n')
367 self.ui.status(m % (srcauthor, self.authors[srcauthor], dstauthor))
367 self.ui.status(m % (srcauthor, self.authors[srcauthor], dstauthor))
368
368
369 afile.close()
369 afile.close()
370
370
371 def cachecommit(self, rev):
371 def cachecommit(self, rev):
372 commit = self.source.getcommit(rev)
372 commit = self.source.getcommit(rev)
373 commit.author = self.authors.get(commit.author, commit.author)
373 commit.author = self.authors.get(commit.author, commit.author)
374 # If commit.branch is None, this commit is coming from the source
374 # If commit.branch is None, this commit is coming from the source
375 # repository's default branch and destined for the default branch in the
375 # repository's default branch and destined for the default branch in the
376 # destination repository. For such commits, passing a literal "None"
376 # destination repository. For such commits, passing a literal "None"
377 # string to branchmap.get() below allows the user to map "None" to an
377 # string to branchmap.get() below allows the user to map "None" to an
378 # alternate default branch in the destination repository.
378 # alternate default branch in the destination repository.
379 commit.branch = self.branchmap.get(str(commit.branch), commit.branch)
379 commit.branch = self.branchmap.get(str(commit.branch), commit.branch)
380 self.commitcache[rev] = commit
380 self.commitcache[rev] = commit
381 return commit
381 return commit
382
382
383 def copy(self, rev):
383 def copy(self, rev):
384 commit = self.commitcache[rev]
384 commit = self.commitcache[rev]
385
385
386 changes = self.source.getchanges(rev)
386 changes = self.source.getchanges(rev)
387 if isinstance(changes, basestring):
387 if isinstance(changes, basestring):
388 if changes == SKIPREV:
388 if changes == SKIPREV:
389 dest = SKIPREV
389 dest = SKIPREV
390 else:
390 else:
391 dest = self.map[changes]
391 dest = self.map[changes]
392 self.map[rev] = dest
392 self.map[rev] = dest
393 return
393 return
394 files, copies = changes
394 files, copies = changes
395 pbranches = []
395 pbranches = []
396 if commit.parents:
396 if commit.parents:
397 for prev in commit.parents:
397 for prev in commit.parents:
398 if prev not in self.commitcache:
398 if prev not in self.commitcache:
399 self.cachecommit(prev)
399 self.cachecommit(prev)
400 pbranches.append((self.map[prev],
400 pbranches.append((self.map[prev],
401 self.commitcache[prev].branch))
401 self.commitcache[prev].branch))
402 self.dest.setbranch(commit.branch, pbranches)
402 self.dest.setbranch(commit.branch, pbranches)
403 try:
403 try:
404 parents = self.splicemap[rev]
404 parents = self.splicemap[rev]
405 self.ui.status(_('spliced in %s as parents of %s\n') %
405 self.ui.status(_('spliced in %s as parents of %s\n') %
406 (parents, rev))
406 (parents, rev))
407 parents = [self.map.get(p, p) for p in parents]
407 parents = [self.map.get(p, p) for p in parents]
408 except KeyError:
408 except KeyError:
409 parents = [b[0] for b in pbranches]
409 parents = [b[0] for b in pbranches]
410 source = progresssource(self.ui, self.source, len(files))
410 source = progresssource(self.ui, self.source, len(files))
411 newnode = self.dest.putcommit(files, copies, parents, commit,
411 newnode = self.dest.putcommit(files, copies, parents, commit,
412 source, self.map)
412 source, self.map)
413 source.close()
413 source.close()
414 self.source.converted(rev, newnode)
414 self.source.converted(rev, newnode)
415 self.map[rev] = newnode
415 self.map[rev] = newnode
416
416
417 def convert(self, sortmode):
417 def convert(self, sortmode):
418 try:
418 try:
419 self.source.before()
419 self.source.before()
420 self.dest.before()
420 self.dest.before()
421 self.source.setrevmap(self.map)
421 self.source.setrevmap(self.map)
422 self.ui.status(_("scanning source...\n"))
422 self.ui.status(_("scanning source...\n"))
423 heads = self.source.getheads()
423 heads = self.source.getheads()
424 parents = self.walktree(heads)
424 parents = self.walktree(heads)
425 self.mergesplicemap(parents, self.splicemap)
425 self.mergesplicemap(parents, self.splicemap)
426 self.ui.status(_("sorting...\n"))
426 self.ui.status(_("sorting...\n"))
427 t = self.toposort(parents, sortmode)
427 t = self.toposort(parents, sortmode)
428 num = len(t)
428 num = len(t)
429 c = None
429 c = None
430
430
431 self.ui.status(_("converting...\n"))
431 self.ui.status(_("converting...\n"))
432 for i, c in enumerate(t):
432 for i, c in enumerate(t):
433 num -= 1
433 num -= 1
434 desc = self.commitcache[c].desc
434 desc = self.commitcache[c].desc
435 if "\n" in desc:
435 if "\n" in desc:
436 desc = desc.splitlines()[0]
436 desc = desc.splitlines()[0]
437 # convert log message to local encoding without using
437 # convert log message to local encoding without using
438 # tolocal() because the encoding.encoding convert()
438 # tolocal() because the encoding.encoding convert()
439 # uses is 'utf-8'
439 # uses is 'utf-8'
440 self.ui.status("%d %s\n" % (num, recode(desc)))
440 self.ui.status("%d %s\n" % (num, recode(desc)))
441 self.ui.note(_("source: %s\n") % recode(c))
441 self.ui.note(_("source: %s\n") % recode(c))
442 self.ui.progress(_('converting'), i, unit=_('revisions'),
442 self.ui.progress(_('converting'), i, unit=_('revisions'),
443 total=len(t))
443 total=len(t))
444 self.copy(c)
444 self.copy(c)
445 self.ui.progress(_('converting'), None)
445 self.ui.progress(_('converting'), None)
446
446
447 tags = self.source.gettags()
447 tags = self.source.gettags()
448 ctags = {}
448 ctags = {}
449 for k in tags:
449 for k in tags:
450 v = tags[k]
450 v = tags[k]
451 if self.map.get(v, SKIPREV) != SKIPREV:
451 if self.map.get(v, SKIPREV) != SKIPREV:
452 ctags[k] = self.map[v]
452 ctags[k] = self.map[v]
453
453
454 if c and ctags:
454 if c and ctags:
455 nrev, tagsparent = self.dest.puttags(ctags)
455 nrev, tagsparent = self.dest.puttags(ctags)
456 if nrev and tagsparent:
456 if nrev and tagsparent:
457 # write another hash correspondence to override the previous
457 # write another hash correspondence to override the previous
458 # one so we don't end up with extra tag heads
458 # one so we don't end up with extra tag heads
459 tagsparents = [e for e in self.map.iteritems()
459 tagsparents = [e for e in self.map.iteritems()
460 if e[1] == tagsparent]
460 if e[1] == tagsparent]
461 if tagsparents:
461 if tagsparents:
462 self.map[tagsparents[0][0]] = nrev
462 self.map[tagsparents[0][0]] = nrev
463
463
464 bookmarks = self.source.getbookmarks()
464 bookmarks = self.source.getbookmarks()
465 cbookmarks = {}
465 cbookmarks = {}
466 for k in bookmarks:
466 for k in bookmarks:
467 v = bookmarks[k]
467 v = bookmarks[k]
468 if self.map.get(v, SKIPREV) != SKIPREV:
468 if self.map.get(v, SKIPREV) != SKIPREV:
469 cbookmarks[k] = self.map[v]
469 cbookmarks[k] = self.map[v]
470
470
471 if c and cbookmarks:
471 if c and cbookmarks:
472 self.dest.putbookmarks(cbookmarks)
472 self.dest.putbookmarks(cbookmarks)
473
473
474 self.writeauthormap()
474 self.writeauthormap()
475 finally:
475 finally:
476 self.cleanup()
476 self.cleanup()
477
477
478 def cleanup(self):
478 def cleanup(self):
479 try:
479 try:
480 self.dest.after()
480 self.dest.after()
481 finally:
481 finally:
482 self.source.after()
482 self.source.after()
483 self.map.close()
483 self.map.close()
484
484
485 def convert(ui, src, dest=None, revmapfile=None, **opts):
485 def convert(ui, src, dest=None, revmapfile=None, **opts):
486 global orig_encoding
486 global orig_encoding
487 orig_encoding = encoding.encoding
487 orig_encoding = encoding.encoding
488 encoding.encoding = 'UTF-8'
488 encoding.encoding = 'UTF-8'
489
489
490 # support --authors as an alias for --authormap
490 # support --authors as an alias for --authormap
491 if not opts.get('authormap'):
491 if not opts.get('authormap'):
492 opts['authormap'] = opts.get('authors')
492 opts['authormap'] = opts.get('authors')
493
493
494 if not dest:
494 if not dest:
495 dest = hg.defaultdest(src) + "-hg"
495 dest = hg.defaultdest(src) + "-hg"
496 ui.status(_("assuming destination %s\n") % dest)
496 ui.status(_("assuming destination %s\n") % dest)
497
497
498 destc = convertsink(ui, dest, opts.get('dest_type'))
498 destc = convertsink(ui, dest, opts.get('dest_type'))
499
499
500 try:
500 try:
501 srcc, defaultsort = convertsource(ui, src, opts.get('source_type'),
501 srcc, defaultsort = convertsource(ui, src, opts.get('source_type'),
502 opts.get('rev'))
502 opts.get('rev'))
503 except Exception:
503 except Exception:
504 for path in destc.created:
504 for path in destc.created:
505 shutil.rmtree(path, True)
505 shutil.rmtree(path, True)
506 raise
506 raise
507
507
508 sortmodes = ('branchsort', 'datesort', 'sourcesort', 'closesort')
508 sortmodes = ('branchsort', 'datesort', 'sourcesort', 'closesort')
509 sortmode = [m for m in sortmodes if opts.get(m)]
509 sortmode = [m for m in sortmodes if opts.get(m)]
510 if len(sortmode) > 1:
510 if len(sortmode) > 1:
511 raise util.Abort(_('more than one sort mode specified'))
511 raise util.Abort(_('more than one sort mode specified'))
512 sortmode = sortmode and sortmode[0] or defaultsort
512 sortmode = sortmode and sortmode[0] or defaultsort
513 if sortmode == 'sourcesort' and not srcc.hasnativeorder():
513 if sortmode == 'sourcesort' and not srcc.hasnativeorder():
514 raise util.Abort(_('--sourcesort is not supported by this data source'))
514 raise util.Abort(_('--sourcesort is not supported by this data source'))
515 if sortmode == 'closesort' and not srcc.hasnativeclose():
515 if sortmode == 'closesort' and not srcc.hasnativeclose():
516 raise util.Abort(_('--closesort is not supported by this data source'))
516 raise util.Abort(_('--closesort is not supported by this data source'))
517
517
518 fmap = opts.get('filemap')
518 fmap = opts.get('filemap')
519 if fmap:
519 if fmap:
520 srcc = filemap.filemap_source(ui, srcc, fmap)
520 srcc = filemap.filemap_source(ui, srcc, fmap)
521 destc.setfilemapmode(True)
521 destc.setfilemapmode(True)
522
522
523 if not revmapfile:
523 if not revmapfile:
524 revmapfile = destc.revmapfile()
524 revmapfile = destc.revmapfile()
525
525
526 c = converter(ui, srcc, destc, revmapfile, opts)
526 c = converter(ui, srcc, destc, revmapfile, opts)
527 c.convert(sortmode)
527 c.convert(sortmode)
528
528
@@ -1,448 +1,448 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
19
20
20
21 import os, time, cStringIO
21 import os, time, cStringIO
22 from mercurial.i18n import _
22 from mercurial.i18n import _
23 from mercurial.node import bin, hex, nullid
23 from mercurial.node import bin, hex, nullid
24 from mercurial import hg, util, context, bookmarks, error, scmutil
24 from mercurial import hg, util, context, bookmarks, error, scmutil
25
25
26 from common import NoRepo, commit, converter_source, converter_sink
26 from common import NoRepo, commit, converter_source, converter_sink
27
27
28 import re
28 import re
29 sha1re = re.compile(r'\b[0-9a-f]{6,40}\b')
29 sha1re = re.compile(r'\b[0-9a-f]{6,40}\b')
30
30
31 class mercurial_sink(converter_sink):
31 class mercurial_sink(converter_sink):
32 def __init__(self, ui, path):
32 def __init__(self, ui, path):
33 converter_sink.__init__(self, ui, path)
33 converter_sink.__init__(self, ui, path)
34 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
34 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
35 self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
35 self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
36 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
36 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
37 self.lastbranch = None
37 self.lastbranch = None
38 if os.path.isdir(path) and len(os.listdir(path)) > 0:
38 if os.path.isdir(path) and len(os.listdir(path)) > 0:
39 try:
39 try:
40 self.repo = hg.repository(self.ui, path)
40 self.repo = hg.repository(self.ui, path)
41 if not self.repo.local():
41 if not self.repo.local():
42 raise NoRepo(_('%s is not a local Mercurial repository')
42 raise NoRepo(_('%s is not a local Mercurial repository')
43 % path)
43 % path)
44 except error.RepoError, err:
44 except error.RepoError, err:
45 ui.traceback()
45 ui.traceback()
46 raise NoRepo(err.args[0])
46 raise NoRepo(err.args[0])
47 else:
47 else:
48 try:
48 try:
49 ui.status(_('initializing destination %s repository\n') % path)
49 ui.status(_('initializing destination %s repository\n') % path)
50 self.repo = hg.repository(self.ui, path, create=True)
50 self.repo = hg.repository(self.ui, path, create=True)
51 if not self.repo.local():
51 if not self.repo.local():
52 raise NoRepo(_('%s is not a local Mercurial repository')
52 raise NoRepo(_('%s is not a local Mercurial repository')
53 % path)
53 % path)
54 self.created.append(path)
54 self.created.append(path)
55 except error.RepoError:
55 except error.RepoError:
56 ui.traceback()
56 ui.traceback()
57 raise NoRepo(_("could not create hg repository %s as sink")
57 raise NoRepo(_("could not create hg repository %s as sink")
58 % path)
58 % path)
59 self.lock = None
59 self.lock = None
60 self.wlock = None
60 self.wlock = None
61 self.filemapmode = False
61 self.filemapmode = False
62
62
63 def before(self):
63 def before(self):
64 self.ui.debug('run hg sink pre-conversion action\n')
64 self.ui.debug('run hg sink pre-conversion action\n')
65 self.wlock = self.repo.wlock()
65 self.wlock = self.repo.wlock()
66 self.lock = self.repo.lock()
66 self.lock = self.repo.lock()
67
67
68 def after(self):
68 def after(self):
69 self.ui.debug('run hg sink post-conversion action\n')
69 self.ui.debug('run hg sink post-conversion action\n')
70 if self.lock:
70 if self.lock:
71 self.lock.release()
71 self.lock.release()
72 if self.wlock:
72 if self.wlock:
73 self.wlock.release()
73 self.wlock.release()
74
74
75 def revmapfile(self):
75 def revmapfile(self):
76 return self.repo.join("shamap")
76 return self.repo.join("shamap")
77
77
78 def authorfile(self):
78 def authorfile(self):
79 return self.repo.join("authormap")
79 return self.repo.join("authormap")
80
80
81 def setbranch(self, branch, pbranches):
81 def setbranch(self, branch, pbranches):
82 if not self.clonebranches:
82 if not self.clonebranches:
83 return
83 return
84
84
85 setbranch = (branch != self.lastbranch)
85 setbranch = (branch != self.lastbranch)
86 self.lastbranch = branch
86 self.lastbranch = branch
87 if not branch:
87 if not branch:
88 branch = 'default'
88 branch = 'default'
89 pbranches = [(b[0], b[1] and b[1] or 'default') for b in pbranches]
89 pbranches = [(b[0], b[1] and b[1] or 'default') for b in pbranches]
90 pbranch = pbranches and pbranches[0][1] or 'default'
90 pbranch = pbranches and pbranches[0][1] or 'default'
91
91
92 branchpath = os.path.join(self.path, branch)
92 branchpath = os.path.join(self.path, branch)
93 if setbranch:
93 if setbranch:
94 self.after()
94 self.after()
95 try:
95 try:
96 self.repo = hg.repository(self.ui, branchpath)
96 self.repo = hg.repository(self.ui, branchpath)
97 except Exception:
97 except Exception:
98 self.repo = hg.repository(self.ui, branchpath, create=True)
98 self.repo = hg.repository(self.ui, branchpath, create=True)
99 self.before()
99 self.before()
100
100
101 # pbranches may bring revisions from other branches (merge parents)
101 # pbranches may bring revisions from other branches (merge parents)
102 # Make sure we have them, or pull them.
102 # Make sure we have them, or pull them.
103 missings = {}
103 missings = {}
104 for b in pbranches:
104 for b in pbranches:
105 try:
105 try:
106 self.repo.lookup(b[0])
106 self.repo.lookup(b[0])
107 except Exception:
107 except Exception:
108 missings.setdefault(b[1], []).append(b[0])
108 missings.setdefault(b[1], []).append(b[0])
109
109
110 if missings:
110 if missings:
111 self.after()
111 self.after()
112 for pbranch, heads in sorted(missings.iteritems()):
112 for pbranch, heads in sorted(missings.iteritems()):
113 pbranchpath = os.path.join(self.path, pbranch)
113 pbranchpath = os.path.join(self.path, pbranch)
114 prepo = hg.peer(self.ui, {}, pbranchpath)
114 prepo = hg.peer(self.ui, {}, pbranchpath)
115 self.ui.note(_('pulling from %s into %s\n') % (pbranch, branch))
115 self.ui.note(_('pulling from %s into %s\n') % (pbranch, branch))
116 self.repo.pull(prepo, [prepo.lookup(h) for h in heads])
116 self.repo.pull(prepo, [prepo.lookup(h) for h in heads])
117 self.before()
117 self.before()
118
118
119 def _rewritetags(self, source, revmap, data):
119 def _rewritetags(self, source, revmap, data):
120 fp = cStringIO.StringIO()
120 fp = cStringIO.StringIO()
121 for line in data.splitlines():
121 for line in data.splitlines():
122 s = line.split(' ', 1)
122 s = line.split(' ', 1)
123 if len(s) != 2:
123 if len(s) != 2:
124 continue
124 continue
125 revid = revmap.get(source.lookuprev(s[0]))
125 revid = revmap.get(source.lookuprev(s[0]))
126 if not revid:
126 if not revid:
127 continue
127 continue
128 fp.write('%s %s\n' % (revid, s[1]))
128 fp.write('%s %s\n' % (revid, s[1]))
129 return fp.getvalue()
129 return fp.getvalue()
130
130
131 def putcommit(self, files, copies, parents, commit, source, revmap):
131 def putcommit(self, files, copies, parents, commit, source, revmap):
132
132
133 files = dict(files)
133 files = dict(files)
134 def getfilectx(repo, memctx, f):
134 def getfilectx(repo, memctx, f):
135 v = files[f]
135 v = files[f]
136 data, mode = source.getfile(f, v)
136 data, mode = source.getfile(f, v)
137 if f == '.hgtags':
137 if f == '.hgtags':
138 data = self._rewritetags(source, revmap, data)
138 data = self._rewritetags(source, revmap, data)
139 return context.memfilectx(f, data, 'l' in mode, 'x' in mode,
139 return context.memfilectx(f, data, 'l' in mode, 'x' in mode,
140 copies.get(f))
140 copies.get(f))
141
141
142 pl = []
142 pl = []
143 for p in parents:
143 for p in parents:
144 if p not in pl:
144 if p not in pl:
145 pl.append(p)
145 pl.append(p)
146 parents = pl
146 parents = pl
147 nparents = len(parents)
147 nparents = len(parents)
148 if self.filemapmode and nparents == 1:
148 if self.filemapmode and nparents == 1:
149 m1node = self.repo.changelog.read(bin(parents[0]))[0]
149 m1node = self.repo.changelog.read(bin(parents[0]))[0]
150 parent = parents[0]
150 parent = parents[0]
151
151
152 if len(parents) < 2:
152 if len(parents) < 2:
153 parents.append(nullid)
153 parents.append(nullid)
154 if len(parents) < 2:
154 if len(parents) < 2:
155 parents.append(nullid)
155 parents.append(nullid)
156 p2 = parents.pop(0)
156 p2 = parents.pop(0)
157
157
158 text = commit.desc
158 text = commit.desc
159
159
160 sha1s = re.findall(sha1re, text)
160 sha1s = re.findall(sha1re, text)
161 for sha1 in sha1s:
161 for sha1 in sha1s:
162 oldrev = source.lookuprev(sha1)
162 oldrev = source.lookuprev(sha1)
163 newrev = revmap.get(oldrev)
163 newrev = revmap.get(oldrev)
164 if newrev is not None:
164 if newrev is not None:
165 text = text.replace(sha1, newrev[:len(sha1)])
165 text = text.replace(sha1, newrev[:len(sha1)])
166
166
167 extra = commit.extra.copy()
167 extra = commit.extra.copy()
168 if self.branchnames and commit.branch:
168 if self.branchnames and commit.branch:
169 extra['branch'] = commit.branch
169 extra['branch'] = commit.branch
170 if commit.rev:
170 if commit.rev:
171 extra['convert_revision'] = commit.rev
171 extra['convert_revision'] = commit.rev
172
172
173 while parents:
173 while parents:
174 p1 = p2
174 p1 = p2
175 p2 = parents.pop(0)
175 p2 = parents.pop(0)
176 ctx = context.memctx(self.repo, (p1, p2), text, files.keys(),
176 ctx = context.memctx(self.repo, (p1, p2), text, files.keys(),
177 getfilectx, commit.author, commit.date, extra)
177 getfilectx, commit.author, commit.date, extra)
178 self.repo.commitctx(ctx)
178 self.repo.commitctx(ctx)
179 text = "(octopus merge fixup)\n"
179 text = "(octopus merge fixup)\n"
180 p2 = hex(self.repo.changelog.tip())
180 p2 = hex(self.repo.changelog.tip())
181
181
182 if self.filemapmode and nparents == 1:
182 if self.filemapmode and nparents == 1:
183 man = self.repo.manifest
183 man = self.repo.manifest
184 mnode = self.repo.changelog.read(bin(p2))[0]
184 mnode = self.repo.changelog.read(bin(p2))[0]
185 closed = 'close' in commit.extra
185 closed = 'close' in commit.extra
186 if not closed and not man.cmp(m1node, man.revision(mnode)):
186 if not closed and not man.cmp(m1node, man.revision(mnode)):
187 self.ui.status(_("filtering out empty revision\n"))
187 self.ui.status(_("filtering out empty revision\n"))
188 self.repo.rollback(force=True)
188 self.repo.rollback(force=True)
189 return parent
189 return parent
190 return p2
190 return p2
191
191
192 def puttags(self, tags):
192 def puttags(self, tags):
193 try:
193 try:
194 parentctx = self.repo[self.tagsbranch]
194 parentctx = self.repo[self.tagsbranch]
195 tagparent = parentctx.node()
195 tagparent = parentctx.node()
196 except error.RepoError:
196 except error.RepoError:
197 parentctx = None
197 parentctx = None
198 tagparent = nullid
198 tagparent = nullid
199
199
200 oldlines = set()
200 oldlines = set()
201 for branch, heads in self.repo.branchmap().iteritems():
201 for branch, heads in self.repo.branchmap().iteritems():
202 for h in heads:
202 for h in heads:
203 if '.hgtags' in self.repo[h]:
203 if '.hgtags' in self.repo[h]:
204 oldlines.update(
204 oldlines.update(
205 set(self.repo[h]['.hgtags'].data().splitlines(True)))
205 set(self.repo[h]['.hgtags'].data().splitlines(True)))
206 oldlines = sorted(list(oldlines))
206 oldlines = sorted(list(oldlines))
207
207
208 newlines = sorted([("%s %s\n" % (tags[tag], tag)) for tag in tags])
208 newlines = sorted([("%s %s\n" % (tags[tag], tag)) for tag in tags])
209 if newlines == oldlines:
209 if newlines == oldlines:
210 return None, None
210 return None, None
211
211
212 # if the old and new tags match, then there is nothing to update
212 # if the old and new tags match, then there is nothing to update
213 oldtags = set()
213 oldtags = set()
214 newtags = set()
214 newtags = set()
215 for line in oldlines:
215 for line in oldlines:
216 s = line.strip().split(' ', 1)
216 s = line.strip().split(' ', 1)
217 if len(s) != 2:
217 if len(s) != 2:
218 continue
218 continue
219 oldtags.add(s[1])
219 oldtags.add(s[1])
220 for line in newlines:
220 for line in newlines:
221 s = line.strip().split(' ', 1)
221 s = line.strip().split(' ', 1)
222 if len(s) != 2:
222 if len(s) != 2:
223 continue
223 continue
224 if s[1] not in oldtags:
224 if s[1] not in oldtags:
225 newtags.add(s[1].strip())
225 newtags.add(s[1].strip())
226
226
227 if not newtags:
227 if not newtags:
228 return None, None
228 return None, None
229
229
230 data = "".join(newlines)
230 data = "".join(newlines)
231 def getfilectx(repo, memctx, f):
231 def getfilectx(repo, memctx, f):
232 return context.memfilectx(f, data, False, False, None)
232 return context.memfilectx(f, data, False, False, None)
233
233
234 self.ui.status(_("updating tags\n"))
234 self.ui.status(_("updating tags\n"))
235 date = "%s 0" % int(time.mktime(time.gmtime()))
235 date = "%s 0" % int(time.mktime(time.gmtime()))
236 extra = {'branch': self.tagsbranch}
236 extra = {'branch': self.tagsbranch}
237 ctx = context.memctx(self.repo, (tagparent, None), "update tags",
237 ctx = context.memctx(self.repo, (tagparent, None), "update tags",
238 [".hgtags"], getfilectx, "convert-repo", date,
238 [".hgtags"], getfilectx, "convert-repo", date,
239 extra)
239 extra)
240 self.repo.commitctx(ctx)
240 self.repo.commitctx(ctx)
241 return hex(self.repo.changelog.tip()), hex(tagparent)
241 return hex(self.repo.changelog.tip()), hex(tagparent)
242
242
243 def setfilemapmode(self, active):
243 def setfilemapmode(self, active):
244 self.filemapmode = active
244 self.filemapmode = active
245
245
246 def putbookmarks(self, updatedbookmark):
246 def putbookmarks(self, updatedbookmark):
247 if not len(updatedbookmark):
247 if not len(updatedbookmark):
248 return
248 return
249
249
250 self.ui.status(_("updating bookmarks\n"))
250 self.ui.status(_("updating bookmarks\n"))
251 destmarks = self.repo._bookmarks
251 destmarks = self.repo._bookmarks
252 for bookmark in updatedbookmark:
252 for bookmark in updatedbookmark:
253 destmarks[bookmark] = bin(updatedbookmark[bookmark])
253 destmarks[bookmark] = bin(updatedbookmark[bookmark])
254 destmarks.write()
254 destmarks.write()
255
255
256 def hascommit(self, rev):
256 def hascommitforsplicemap(self, rev):
257 if rev not in self.repo and self.clonebranches:
257 if rev not in self.repo and self.clonebranches:
258 raise util.Abort(_('revision %s not found in destination '
258 raise util.Abort(_('revision %s not found in destination '
259 'repository (lookups with clonebranches=true '
259 'repository (lookups with clonebranches=true '
260 'are not implemented)') % rev)
260 'are not implemented)') % rev)
261 return rev in self.repo
261 return rev in self.repo
262
262
263 class mercurial_source(converter_source):
263 class mercurial_source(converter_source):
264 def __init__(self, ui, path, rev=None):
264 def __init__(self, ui, path, rev=None):
265 converter_source.__init__(self, ui, path, rev)
265 converter_source.__init__(self, ui, path, rev)
266 self.ignoreerrors = ui.configbool('convert', 'hg.ignoreerrors', False)
266 self.ignoreerrors = ui.configbool('convert', 'hg.ignoreerrors', False)
267 self.ignored = set()
267 self.ignored = set()
268 self.saverev = ui.configbool('convert', 'hg.saverev', False)
268 self.saverev = ui.configbool('convert', 'hg.saverev', False)
269 try:
269 try:
270 self.repo = hg.repository(self.ui, path)
270 self.repo = hg.repository(self.ui, path)
271 # try to provoke an exception if this isn't really a hg
271 # try to provoke an exception if this isn't really a hg
272 # repo, but some other bogus compatible-looking url
272 # repo, but some other bogus compatible-looking url
273 if not self.repo.local():
273 if not self.repo.local():
274 raise error.RepoError
274 raise error.RepoError
275 except error.RepoError:
275 except error.RepoError:
276 ui.traceback()
276 ui.traceback()
277 raise NoRepo(_("%s is not a local Mercurial repository") % path)
277 raise NoRepo(_("%s is not a local Mercurial repository") % path)
278 self.lastrev = None
278 self.lastrev = None
279 self.lastctx = None
279 self.lastctx = None
280 self._changescache = None
280 self._changescache = None
281 self.convertfp = None
281 self.convertfp = None
282 # Restrict converted revisions to startrev descendants
282 # Restrict converted revisions to startrev descendants
283 startnode = ui.config('convert', 'hg.startrev')
283 startnode = ui.config('convert', 'hg.startrev')
284 hgrevs = ui.config('convert', 'hg.revs')
284 hgrevs = ui.config('convert', 'hg.revs')
285 if hgrevs is None:
285 if hgrevs is None:
286 if startnode is not None:
286 if startnode is not None:
287 try:
287 try:
288 startnode = self.repo.lookup(startnode)
288 startnode = self.repo.lookup(startnode)
289 except error.RepoError:
289 except error.RepoError:
290 raise util.Abort(_('%s is not a valid start revision')
290 raise util.Abort(_('%s is not a valid start revision')
291 % startnode)
291 % startnode)
292 startrev = self.repo.changelog.rev(startnode)
292 startrev = self.repo.changelog.rev(startnode)
293 children = {startnode: 1}
293 children = {startnode: 1}
294 for r in self.repo.changelog.descendants([startrev]):
294 for r in self.repo.changelog.descendants([startrev]):
295 children[self.repo.changelog.node(r)] = 1
295 children[self.repo.changelog.node(r)] = 1
296 self.keep = children.__contains__
296 self.keep = children.__contains__
297 else:
297 else:
298 self.keep = util.always
298 self.keep = util.always
299 if rev:
299 if rev:
300 self._heads = [self.repo[rev].node()]
300 self._heads = [self.repo[rev].node()]
301 else:
301 else:
302 self._heads = self.repo.heads()
302 self._heads = self.repo.heads()
303 else:
303 else:
304 if rev or startnode is not None:
304 if rev or startnode is not None:
305 raise util.Abort(_('hg.revs cannot be combined with '
305 raise util.Abort(_('hg.revs cannot be combined with '
306 'hg.startrev or --rev'))
306 'hg.startrev or --rev'))
307 nodes = set()
307 nodes = set()
308 parents = set()
308 parents = set()
309 for r in scmutil.revrange(self.repo, [hgrevs]):
309 for r in scmutil.revrange(self.repo, [hgrevs]):
310 ctx = self.repo[r]
310 ctx = self.repo[r]
311 nodes.add(ctx.node())
311 nodes.add(ctx.node())
312 parents.update(p.node() for p in ctx.parents())
312 parents.update(p.node() for p in ctx.parents())
313 self.keep = nodes.__contains__
313 self.keep = nodes.__contains__
314 self._heads = nodes - parents
314 self._heads = nodes - parents
315
315
316 def changectx(self, rev):
316 def changectx(self, rev):
317 if self.lastrev != rev:
317 if self.lastrev != rev:
318 self.lastctx = self.repo[rev]
318 self.lastctx = self.repo[rev]
319 self.lastrev = rev
319 self.lastrev = rev
320 return self.lastctx
320 return self.lastctx
321
321
322 def parents(self, ctx):
322 def parents(self, ctx):
323 return [p for p in ctx.parents() if p and self.keep(p.node())]
323 return [p for p in ctx.parents() if p and self.keep(p.node())]
324
324
325 def getheads(self):
325 def getheads(self):
326 return [hex(h) for h in self._heads if self.keep(h)]
326 return [hex(h) for h in self._heads if self.keep(h)]
327
327
328 def getfile(self, name, rev):
328 def getfile(self, name, rev):
329 try:
329 try:
330 fctx = self.changectx(rev)[name]
330 fctx = self.changectx(rev)[name]
331 return fctx.data(), fctx.flags()
331 return fctx.data(), fctx.flags()
332 except error.LookupError, err:
332 except error.LookupError, err:
333 raise IOError(err)
333 raise IOError(err)
334
334
335 def getchanges(self, rev):
335 def getchanges(self, rev):
336 ctx = self.changectx(rev)
336 ctx = self.changectx(rev)
337 parents = self.parents(ctx)
337 parents = self.parents(ctx)
338 if not parents:
338 if not parents:
339 files = sorted(ctx.manifest())
339 files = sorted(ctx.manifest())
340 # getcopies() is not needed for roots, but it is a simple way to
340 # getcopies() is not needed for roots, but it is a simple way to
341 # detect missing revlogs and abort on errors or populate
341 # detect missing revlogs and abort on errors or populate
342 # self.ignored
342 # self.ignored
343 self.getcopies(ctx, parents, files)
343 self.getcopies(ctx, parents, files)
344 return [(f, rev) for f in files if f not in self.ignored], {}
344 return [(f, rev) for f in files if f not in self.ignored], {}
345 if self._changescache and self._changescache[0] == rev:
345 if self._changescache and self._changescache[0] == rev:
346 m, a, r = self._changescache[1]
346 m, a, r = self._changescache[1]
347 else:
347 else:
348 m, a, r = self.repo.status(parents[0].node(), ctx.node())[:3]
348 m, a, r = self.repo.status(parents[0].node(), ctx.node())[:3]
349 # getcopies() detects missing revlogs early, run it before
349 # getcopies() detects missing revlogs early, run it before
350 # filtering the changes.
350 # filtering the changes.
351 copies = self.getcopies(ctx, parents, m + a)
351 copies = self.getcopies(ctx, parents, m + a)
352 changes = [(name, rev) for name in m + a + r
352 changes = [(name, rev) for name in m + a + r
353 if name not in self.ignored]
353 if name not in self.ignored]
354 return sorted(changes), copies
354 return sorted(changes), copies
355
355
356 def getcopies(self, ctx, parents, files):
356 def getcopies(self, ctx, parents, files):
357 copies = {}
357 copies = {}
358 for name in files:
358 for name in files:
359 if name in self.ignored:
359 if name in self.ignored:
360 continue
360 continue
361 try:
361 try:
362 copysource, _copynode = ctx.filectx(name).renamed()
362 copysource, _copynode = ctx.filectx(name).renamed()
363 if copysource in self.ignored:
363 if copysource in self.ignored:
364 continue
364 continue
365 # Ignore copy sources not in parent revisions
365 # Ignore copy sources not in parent revisions
366 found = False
366 found = False
367 for p in parents:
367 for p in parents:
368 if copysource in p:
368 if copysource in p:
369 found = True
369 found = True
370 break
370 break
371 if not found:
371 if not found:
372 continue
372 continue
373 copies[name] = copysource
373 copies[name] = copysource
374 except TypeError:
374 except TypeError:
375 pass
375 pass
376 except error.LookupError, e:
376 except error.LookupError, e:
377 if not self.ignoreerrors:
377 if not self.ignoreerrors:
378 raise
378 raise
379 self.ignored.add(name)
379 self.ignored.add(name)
380 self.ui.warn(_('ignoring: %s\n') % e)
380 self.ui.warn(_('ignoring: %s\n') % e)
381 return copies
381 return copies
382
382
383 def getcommit(self, rev):
383 def getcommit(self, rev):
384 ctx = self.changectx(rev)
384 ctx = self.changectx(rev)
385 parents = [p.hex() for p in self.parents(ctx)]
385 parents = [p.hex() for p in self.parents(ctx)]
386 if self.saverev:
386 if self.saverev:
387 crev = rev
387 crev = rev
388 else:
388 else:
389 crev = None
389 crev = None
390 return commit(author=ctx.user(),
390 return commit(author=ctx.user(),
391 date=util.datestr(ctx.date(), '%Y-%m-%d %H:%M:%S %1%2'),
391 date=util.datestr(ctx.date(), '%Y-%m-%d %H:%M:%S %1%2'),
392 desc=ctx.description(), rev=crev, parents=parents,
392 desc=ctx.description(), rev=crev, parents=parents,
393 branch=ctx.branch(), extra=ctx.extra(),
393 branch=ctx.branch(), extra=ctx.extra(),
394 sortkey=ctx.rev())
394 sortkey=ctx.rev())
395
395
396 def gettags(self):
396 def gettags(self):
397 # This will get written to .hgtags, filter non global tags out.
397 # This will get written to .hgtags, filter non global tags out.
398 tags = [t for t in self.repo.tagslist()
398 tags = [t for t in self.repo.tagslist()
399 if self.repo.tagtype(t[0]) == 'global']
399 if self.repo.tagtype(t[0]) == 'global']
400 return dict([(name, hex(node)) for name, node in tags
400 return dict([(name, hex(node)) for name, node in tags
401 if self.keep(node)])
401 if self.keep(node)])
402
402
403 def getchangedfiles(self, rev, i):
403 def getchangedfiles(self, rev, i):
404 ctx = self.changectx(rev)
404 ctx = self.changectx(rev)
405 parents = self.parents(ctx)
405 parents = self.parents(ctx)
406 if not parents and i is None:
406 if not parents and i is None:
407 i = 0
407 i = 0
408 changes = [], ctx.manifest().keys(), []
408 changes = [], ctx.manifest().keys(), []
409 else:
409 else:
410 i = i or 0
410 i = i or 0
411 changes = self.repo.status(parents[i].node(), ctx.node())[:3]
411 changes = self.repo.status(parents[i].node(), ctx.node())[:3]
412 changes = [[f for f in l if f not in self.ignored] for l in changes]
412 changes = [[f for f in l if f not in self.ignored] for l in changes]
413
413
414 if i == 0:
414 if i == 0:
415 self._changescache = (rev, changes)
415 self._changescache = (rev, changes)
416
416
417 return changes[0] + changes[1] + changes[2]
417 return changes[0] + changes[1] + changes[2]
418
418
419 def converted(self, rev, destrev):
419 def converted(self, rev, destrev):
420 if self.convertfp is None:
420 if self.convertfp is None:
421 self.convertfp = open(self.repo.join('shamap'), 'a')
421 self.convertfp = open(self.repo.join('shamap'), 'a')
422 self.convertfp.write('%s %s\n' % (destrev, rev))
422 self.convertfp.write('%s %s\n' % (destrev, rev))
423 self.convertfp.flush()
423 self.convertfp.flush()
424
424
425 def before(self):
425 def before(self):
426 self.ui.debug('run hg source pre-conversion action\n')
426 self.ui.debug('run hg source pre-conversion action\n')
427
427
428 def after(self):
428 def after(self):
429 self.ui.debug('run hg source post-conversion action\n')
429 self.ui.debug('run hg source post-conversion action\n')
430
430
431 def hasnativeorder(self):
431 def hasnativeorder(self):
432 return True
432 return True
433
433
434 def hasnativeclose(self):
434 def hasnativeclose(self):
435 return True
435 return True
436
436
437 def lookuprev(self, rev):
437 def lookuprev(self, rev):
438 try:
438 try:
439 return hex(self.repo.lookup(rev))
439 return hex(self.repo.lookup(rev))
440 except error.RepoError:
440 except error.RepoError:
441 return None
441 return None
442
442
443 def getbookmarks(self):
443 def getbookmarks(self):
444 return bookmarks.listbookmarks(self.repo)
444 return bookmarks.listbookmarks(self.repo)
445
445
446 def checkrevformat(self, revstr, mapname='splicemap'):
446 def checkrevformat(self, revstr, mapname='splicemap'):
447 """ Mercurial, revision string is a 40 byte hex """
447 """ Mercurial, revision string is a 40 byte hex """
448 self.checkhexformat(revstr, mapname)
448 self.checkhexformat(revstr, mapname)
@@ -1,1310 +1,1310 b''
1 # Subversion 1.4/1.5 Python API backend
1 # Subversion 1.4/1.5 Python API backend
2 #
2 #
3 # Copyright(C) 2007 Daniel Holth et al
3 # Copyright(C) 2007 Daniel Holth et al
4
4
5 import os, re, sys, tempfile, urllib, urllib2
5 import os, re, sys, tempfile, urllib, urllib2
6 import xml.dom.minidom
6 import xml.dom.minidom
7 import cPickle as pickle
7 import cPickle as pickle
8
8
9 from mercurial import strutil, scmutil, util, encoding
9 from mercurial import strutil, scmutil, util, encoding
10 from mercurial.i18n import _
10 from mercurial.i18n import _
11
11
12 propertycache = util.propertycache
12 propertycache = util.propertycache
13
13
14 # Subversion stuff. Works best with very recent Python SVN bindings
14 # Subversion stuff. Works best with very recent Python SVN bindings
15 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
15 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
16 # these bindings.
16 # these bindings.
17
17
18 from cStringIO import StringIO
18 from cStringIO import StringIO
19
19
20 from common import NoRepo, MissingTool, commit, encodeargs, decodeargs
20 from common import NoRepo, MissingTool, commit, encodeargs, decodeargs
21 from common import commandline, converter_source, converter_sink, mapfile
21 from common import commandline, converter_source, converter_sink, mapfile
22 from common import makedatetimestamp
22 from common import makedatetimestamp
23
23
24 try:
24 try:
25 from svn.core import SubversionException, Pool
25 from svn.core import SubversionException, Pool
26 import svn
26 import svn
27 import svn.client
27 import svn.client
28 import svn.core
28 import svn.core
29 import svn.ra
29 import svn.ra
30 import svn.delta
30 import svn.delta
31 import transport
31 import transport
32 import warnings
32 import warnings
33 warnings.filterwarnings('ignore',
33 warnings.filterwarnings('ignore',
34 module='svn.core',
34 module='svn.core',
35 category=DeprecationWarning)
35 category=DeprecationWarning)
36
36
37 except ImportError:
37 except ImportError:
38 svn = None
38 svn = None
39
39
40 class SvnPathNotFound(Exception):
40 class SvnPathNotFound(Exception):
41 pass
41 pass
42
42
43 def revsplit(rev):
43 def revsplit(rev):
44 """Parse a revision string and return (uuid, path, revnum).
44 """Parse a revision string and return (uuid, path, revnum).
45 >>> revsplit('svn:a2147622-4a9f-4db4-a8d3-13562ff547b2'
45 >>> revsplit('svn:a2147622-4a9f-4db4-a8d3-13562ff547b2'
46 ... '/proj%20B/mytrunk/mytrunk@1')
46 ... '/proj%20B/mytrunk/mytrunk@1')
47 ('a2147622-4a9f-4db4-a8d3-13562ff547b2', '/proj%20B/mytrunk/mytrunk', 1)
47 ('a2147622-4a9f-4db4-a8d3-13562ff547b2', '/proj%20B/mytrunk/mytrunk', 1)
48 >>> revsplit('svn:8af66a51-67f5-4354-b62c-98d67cc7be1d@1')
48 >>> revsplit('svn:8af66a51-67f5-4354-b62c-98d67cc7be1d@1')
49 ('', '', 1)
49 ('', '', 1)
50 >>> revsplit('@7')
50 >>> revsplit('@7')
51 ('', '', 7)
51 ('', '', 7)
52 >>> revsplit('7')
52 >>> revsplit('7')
53 ('', '', 0)
53 ('', '', 0)
54 >>> revsplit('bad')
54 >>> revsplit('bad')
55 ('', '', 0)
55 ('', '', 0)
56 """
56 """
57 parts = rev.rsplit('@', 1)
57 parts = rev.rsplit('@', 1)
58 revnum = 0
58 revnum = 0
59 if len(parts) > 1:
59 if len(parts) > 1:
60 revnum = int(parts[1])
60 revnum = int(parts[1])
61 parts = parts[0].split('/', 1)
61 parts = parts[0].split('/', 1)
62 uuid = ''
62 uuid = ''
63 mod = ''
63 mod = ''
64 if len(parts) > 1 and parts[0].startswith('svn:'):
64 if len(parts) > 1 and parts[0].startswith('svn:'):
65 uuid = parts[0][4:]
65 uuid = parts[0][4:]
66 mod = '/' + parts[1]
66 mod = '/' + parts[1]
67 return uuid, mod, revnum
67 return uuid, mod, revnum
68
68
69 def quote(s):
69 def quote(s):
70 # As of svn 1.7, many svn calls expect "canonical" paths. In
70 # As of svn 1.7, many svn calls expect "canonical" paths. In
71 # theory, we should call svn.core.*canonicalize() on all paths
71 # theory, we should call svn.core.*canonicalize() on all paths
72 # before passing them to the API. Instead, we assume the base url
72 # before passing them to the API. Instead, we assume the base url
73 # is canonical and copy the behaviour of svn URL encoding function
73 # is canonical and copy the behaviour of svn URL encoding function
74 # so we can extend it safely with new components. The "safe"
74 # so we can extend it safely with new components. The "safe"
75 # characters were taken from the "svn_uri__char_validity" table in
75 # characters were taken from the "svn_uri__char_validity" table in
76 # libsvn_subr/path.c.
76 # libsvn_subr/path.c.
77 return urllib.quote(s, "!$&'()*+,-./:=@_~")
77 return urllib.quote(s, "!$&'()*+,-./:=@_~")
78
78
79 def geturl(path):
79 def geturl(path):
80 try:
80 try:
81 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
81 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
82 except SubversionException:
82 except SubversionException:
83 # svn.client.url_from_path() fails with local repositories
83 # svn.client.url_from_path() fails with local repositories
84 pass
84 pass
85 if os.path.isdir(path):
85 if os.path.isdir(path):
86 path = os.path.normpath(os.path.abspath(path))
86 path = os.path.normpath(os.path.abspath(path))
87 if os.name == 'nt':
87 if os.name == 'nt':
88 path = '/' + util.normpath(path)
88 path = '/' + util.normpath(path)
89 # Module URL is later compared with the repository URL returned
89 # Module URL is later compared with the repository URL returned
90 # by svn API, which is UTF-8.
90 # by svn API, which is UTF-8.
91 path = encoding.tolocal(path)
91 path = encoding.tolocal(path)
92 path = 'file://%s' % quote(path)
92 path = 'file://%s' % quote(path)
93 return svn.core.svn_path_canonicalize(path)
93 return svn.core.svn_path_canonicalize(path)
94
94
95 def optrev(number):
95 def optrev(number):
96 optrev = svn.core.svn_opt_revision_t()
96 optrev = svn.core.svn_opt_revision_t()
97 optrev.kind = svn.core.svn_opt_revision_number
97 optrev.kind = svn.core.svn_opt_revision_number
98 optrev.value.number = number
98 optrev.value.number = number
99 return optrev
99 return optrev
100
100
101 class changedpath(object):
101 class changedpath(object):
102 def __init__(self, p):
102 def __init__(self, p):
103 self.copyfrom_path = p.copyfrom_path
103 self.copyfrom_path = p.copyfrom_path
104 self.copyfrom_rev = p.copyfrom_rev
104 self.copyfrom_rev = p.copyfrom_rev
105 self.action = p.action
105 self.action = p.action
106
106
107 def get_log_child(fp, url, paths, start, end, limit=0,
107 def get_log_child(fp, url, paths, start, end, limit=0,
108 discover_changed_paths=True, strict_node_history=False):
108 discover_changed_paths=True, strict_node_history=False):
109 protocol = -1
109 protocol = -1
110 def receiver(orig_paths, revnum, author, date, message, pool):
110 def receiver(orig_paths, revnum, author, date, message, pool):
111 paths = {}
111 paths = {}
112 if orig_paths is not None:
112 if orig_paths is not None:
113 for k, v in orig_paths.iteritems():
113 for k, v in orig_paths.iteritems():
114 paths[k] = changedpath(v)
114 paths[k] = changedpath(v)
115 pickle.dump((paths, revnum, author, date, message),
115 pickle.dump((paths, revnum, author, date, message),
116 fp, protocol)
116 fp, protocol)
117
117
118 try:
118 try:
119 # Use an ra of our own so that our parent can consume
119 # Use an ra of our own so that our parent can consume
120 # our results without confusing the server.
120 # our results without confusing the server.
121 t = transport.SvnRaTransport(url=url)
121 t = transport.SvnRaTransport(url=url)
122 svn.ra.get_log(t.ra, paths, start, end, limit,
122 svn.ra.get_log(t.ra, paths, start, end, limit,
123 discover_changed_paths,
123 discover_changed_paths,
124 strict_node_history,
124 strict_node_history,
125 receiver)
125 receiver)
126 except IOError:
126 except IOError:
127 # Caller may interrupt the iteration
127 # Caller may interrupt the iteration
128 pickle.dump(None, fp, protocol)
128 pickle.dump(None, fp, protocol)
129 except Exception, inst:
129 except Exception, inst:
130 pickle.dump(str(inst), fp, protocol)
130 pickle.dump(str(inst), fp, protocol)
131 else:
131 else:
132 pickle.dump(None, fp, protocol)
132 pickle.dump(None, fp, protocol)
133 fp.close()
133 fp.close()
134 # With large history, cleanup process goes crazy and suddenly
134 # With large history, cleanup process goes crazy and suddenly
135 # consumes *huge* amount of memory. The output file being closed,
135 # consumes *huge* amount of memory. The output file being closed,
136 # there is no need for clean termination.
136 # there is no need for clean termination.
137 os._exit(0)
137 os._exit(0)
138
138
139 def debugsvnlog(ui, **opts):
139 def debugsvnlog(ui, **opts):
140 """Fetch SVN log in a subprocess and channel them back to parent to
140 """Fetch SVN log in a subprocess and channel them back to parent to
141 avoid memory collection issues.
141 avoid memory collection issues.
142 """
142 """
143 if svn is None:
143 if svn is None:
144 raise util.Abort(_('debugsvnlog could not load Subversion python '
144 raise util.Abort(_('debugsvnlog could not load Subversion python '
145 'bindings'))
145 'bindings'))
146
146
147 util.setbinary(sys.stdin)
147 util.setbinary(sys.stdin)
148 util.setbinary(sys.stdout)
148 util.setbinary(sys.stdout)
149 args = decodeargs(sys.stdin.read())
149 args = decodeargs(sys.stdin.read())
150 get_log_child(sys.stdout, *args)
150 get_log_child(sys.stdout, *args)
151
151
152 class logstream(object):
152 class logstream(object):
153 """Interruptible revision log iterator."""
153 """Interruptible revision log iterator."""
154 def __init__(self, stdout):
154 def __init__(self, stdout):
155 self._stdout = stdout
155 self._stdout = stdout
156
156
157 def __iter__(self):
157 def __iter__(self):
158 while True:
158 while True:
159 try:
159 try:
160 entry = pickle.load(self._stdout)
160 entry = pickle.load(self._stdout)
161 except EOFError:
161 except EOFError:
162 raise util.Abort(_('Mercurial failed to run itself, check'
162 raise util.Abort(_('Mercurial failed to run itself, check'
163 ' hg executable is in PATH'))
163 ' hg executable is in PATH'))
164 try:
164 try:
165 orig_paths, revnum, author, date, message = entry
165 orig_paths, revnum, author, date, message = entry
166 except (TypeError, ValueError):
166 except (TypeError, ValueError):
167 if entry is None:
167 if entry is None:
168 break
168 break
169 raise util.Abort(_("log stream exception '%s'") % entry)
169 raise util.Abort(_("log stream exception '%s'") % entry)
170 yield entry
170 yield entry
171
171
172 def close(self):
172 def close(self):
173 if self._stdout:
173 if self._stdout:
174 self._stdout.close()
174 self._stdout.close()
175 self._stdout = None
175 self._stdout = None
176
176
177 class directlogstream(list):
177 class directlogstream(list):
178 """Direct revision log iterator.
178 """Direct revision log iterator.
179 This can be used for debugging and development but it will probably leak
179 This can be used for debugging and development but it will probably leak
180 memory and is not suitable for real conversions."""
180 memory and is not suitable for real conversions."""
181 def __init__(self, url, paths, start, end, limit=0,
181 def __init__(self, url, paths, start, end, limit=0,
182 discover_changed_paths=True, strict_node_history=False):
182 discover_changed_paths=True, strict_node_history=False):
183
183
184 def receiver(orig_paths, revnum, author, date, message, pool):
184 def receiver(orig_paths, revnum, author, date, message, pool):
185 paths = {}
185 paths = {}
186 if orig_paths is not None:
186 if orig_paths is not None:
187 for k, v in orig_paths.iteritems():
187 for k, v in orig_paths.iteritems():
188 paths[k] = changedpath(v)
188 paths[k] = changedpath(v)
189 self.append((paths, revnum, author, date, message))
189 self.append((paths, revnum, author, date, message))
190
190
191 # Use an ra of our own so that our parent can consume
191 # Use an ra of our own so that our parent can consume
192 # our results without confusing the server.
192 # our results without confusing the server.
193 t = transport.SvnRaTransport(url=url)
193 t = transport.SvnRaTransport(url=url)
194 svn.ra.get_log(t.ra, paths, start, end, limit,
194 svn.ra.get_log(t.ra, paths, start, end, limit,
195 discover_changed_paths,
195 discover_changed_paths,
196 strict_node_history,
196 strict_node_history,
197 receiver)
197 receiver)
198
198
199 def close(self):
199 def close(self):
200 pass
200 pass
201
201
202 # Check to see if the given path is a local Subversion repo. Verify this by
202 # Check to see if the given path is a local Subversion repo. Verify this by
203 # looking for several svn-specific files and directories in the given
203 # looking for several svn-specific files and directories in the given
204 # directory.
204 # directory.
205 def filecheck(ui, path, proto):
205 def filecheck(ui, path, proto):
206 for x in ('locks', 'hooks', 'format', 'db'):
206 for x in ('locks', 'hooks', 'format', 'db'):
207 if not os.path.exists(os.path.join(path, x)):
207 if not os.path.exists(os.path.join(path, x)):
208 return False
208 return False
209 return True
209 return True
210
210
211 # Check to see if a given path is the root of an svn repo over http. We verify
211 # Check to see if a given path is the root of an svn repo over http. We verify
212 # this by requesting a version-controlled URL we know can't exist and looking
212 # this by requesting a version-controlled URL we know can't exist and looking
213 # for the svn-specific "not found" XML.
213 # for the svn-specific "not found" XML.
214 def httpcheck(ui, path, proto):
214 def httpcheck(ui, path, proto):
215 try:
215 try:
216 opener = urllib2.build_opener()
216 opener = urllib2.build_opener()
217 rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path))
217 rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path))
218 data = rsp.read()
218 data = rsp.read()
219 except urllib2.HTTPError, inst:
219 except urllib2.HTTPError, inst:
220 if inst.code != 404:
220 if inst.code != 404:
221 # Except for 404 we cannot know for sure this is not an svn repo
221 # Except for 404 we cannot know for sure this is not an svn repo
222 ui.warn(_('svn: cannot probe remote repository, assume it could '
222 ui.warn(_('svn: cannot probe remote repository, assume it could '
223 'be a subversion repository. Use --source-type if you '
223 'be a subversion repository. Use --source-type if you '
224 'know better.\n'))
224 'know better.\n'))
225 return True
225 return True
226 data = inst.fp.read()
226 data = inst.fp.read()
227 except Exception:
227 except Exception:
228 # Could be urllib2.URLError if the URL is invalid or anything else.
228 # Could be urllib2.URLError if the URL is invalid or anything else.
229 return False
229 return False
230 return '<m:human-readable errcode="160013">' in data
230 return '<m:human-readable errcode="160013">' in data
231
231
232 protomap = {'http': httpcheck,
232 protomap = {'http': httpcheck,
233 'https': httpcheck,
233 'https': httpcheck,
234 'file': filecheck,
234 'file': filecheck,
235 }
235 }
236 def issvnurl(ui, url):
236 def issvnurl(ui, url):
237 try:
237 try:
238 proto, path = url.split('://', 1)
238 proto, path = url.split('://', 1)
239 if proto == 'file':
239 if proto == 'file':
240 if (os.name == 'nt' and path[:1] == '/' and path[1:2].isalpha()
240 if (os.name == 'nt' and path[:1] == '/' and path[1:2].isalpha()
241 and path[2:6].lower() == '%3a/'):
241 and path[2:6].lower() == '%3a/'):
242 path = path[:2] + ':/' + path[6:]
242 path = path[:2] + ':/' + path[6:]
243 path = urllib.url2pathname(path)
243 path = urllib.url2pathname(path)
244 except ValueError:
244 except ValueError:
245 proto = 'file'
245 proto = 'file'
246 path = os.path.abspath(url)
246 path = os.path.abspath(url)
247 if proto == 'file':
247 if proto == 'file':
248 path = util.pconvert(path)
248 path = util.pconvert(path)
249 check = protomap.get(proto, lambda *args: False)
249 check = protomap.get(proto, lambda *args: False)
250 while '/' in path:
250 while '/' in path:
251 if check(ui, path, proto):
251 if check(ui, path, proto):
252 return True
252 return True
253 path = path.rsplit('/', 1)[0]
253 path = path.rsplit('/', 1)[0]
254 return False
254 return False
255
255
256 # SVN conversion code stolen from bzr-svn and tailor
256 # SVN conversion code stolen from bzr-svn and tailor
257 #
257 #
258 # Subversion looks like a versioned filesystem, branches structures
258 # Subversion looks like a versioned filesystem, branches structures
259 # are defined by conventions and not enforced by the tool. First,
259 # are defined by conventions and not enforced by the tool. First,
260 # we define the potential branches (modules) as "trunk" and "branches"
260 # we define the potential branches (modules) as "trunk" and "branches"
261 # children directories. Revisions are then identified by their
261 # children directories. Revisions are then identified by their
262 # module and revision number (and a repository identifier).
262 # module and revision number (and a repository identifier).
263 #
263 #
264 # The revision graph is really a tree (or a forest). By default, a
264 # The revision graph is really a tree (or a forest). By default, a
265 # revision parent is the previous revision in the same module. If the
265 # revision parent is the previous revision in the same module. If the
266 # module directory is copied/moved from another module then the
266 # module directory is copied/moved from another module then the
267 # revision is the module root and its parent the source revision in
267 # revision is the module root and its parent the source revision in
268 # the parent module. A revision has at most one parent.
268 # the parent module. A revision has at most one parent.
269 #
269 #
270 class svn_source(converter_source):
270 class svn_source(converter_source):
271 def __init__(self, ui, url, rev=None):
271 def __init__(self, ui, url, rev=None):
272 super(svn_source, self).__init__(ui, url, rev=rev)
272 super(svn_source, self).__init__(ui, url, rev=rev)
273
273
274 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
274 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
275 (os.path.exists(url) and
275 (os.path.exists(url) and
276 os.path.exists(os.path.join(url, '.svn'))) or
276 os.path.exists(os.path.join(url, '.svn'))) or
277 issvnurl(ui, url)):
277 issvnurl(ui, url)):
278 raise NoRepo(_("%s does not look like a Subversion repository")
278 raise NoRepo(_("%s does not look like a Subversion repository")
279 % url)
279 % url)
280 if svn is None:
280 if svn is None:
281 raise MissingTool(_('could not load Subversion python bindings'))
281 raise MissingTool(_('could not load Subversion python bindings'))
282
282
283 try:
283 try:
284 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
284 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
285 if version < (1, 4):
285 if version < (1, 4):
286 raise MissingTool(_('Subversion python bindings %d.%d found, '
286 raise MissingTool(_('Subversion python bindings %d.%d found, '
287 '1.4 or later required') % version)
287 '1.4 or later required') % version)
288 except AttributeError:
288 except AttributeError:
289 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
289 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
290 'or later required'))
290 'or later required'))
291
291
292 self.lastrevs = {}
292 self.lastrevs = {}
293
293
294 latest = None
294 latest = None
295 try:
295 try:
296 # Support file://path@rev syntax. Useful e.g. to convert
296 # Support file://path@rev syntax. Useful e.g. to convert
297 # deleted branches.
297 # deleted branches.
298 at = url.rfind('@')
298 at = url.rfind('@')
299 if at >= 0:
299 if at >= 0:
300 latest = int(url[at + 1:])
300 latest = int(url[at + 1:])
301 url = url[:at]
301 url = url[:at]
302 except ValueError:
302 except ValueError:
303 pass
303 pass
304 self.url = geturl(url)
304 self.url = geturl(url)
305 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
305 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
306 try:
306 try:
307 self.transport = transport.SvnRaTransport(url=self.url)
307 self.transport = transport.SvnRaTransport(url=self.url)
308 self.ra = self.transport.ra
308 self.ra = self.transport.ra
309 self.ctx = self.transport.client
309 self.ctx = self.transport.client
310 self.baseurl = svn.ra.get_repos_root(self.ra)
310 self.baseurl = svn.ra.get_repos_root(self.ra)
311 # Module is either empty or a repository path starting with
311 # Module is either empty or a repository path starting with
312 # a slash and not ending with a slash.
312 # a slash and not ending with a slash.
313 self.module = urllib.unquote(self.url[len(self.baseurl):])
313 self.module = urllib.unquote(self.url[len(self.baseurl):])
314 self.prevmodule = None
314 self.prevmodule = None
315 self.rootmodule = self.module
315 self.rootmodule = self.module
316 self.commits = {}
316 self.commits = {}
317 self.paths = {}
317 self.paths = {}
318 self.uuid = svn.ra.get_uuid(self.ra)
318 self.uuid = svn.ra.get_uuid(self.ra)
319 except SubversionException:
319 except SubversionException:
320 ui.traceback()
320 ui.traceback()
321 raise NoRepo(_("%s does not look like a Subversion repository")
321 raise NoRepo(_("%s does not look like a Subversion repository")
322 % self.url)
322 % self.url)
323
323
324 if rev:
324 if rev:
325 try:
325 try:
326 latest = int(rev)
326 latest = int(rev)
327 except ValueError:
327 except ValueError:
328 raise util.Abort(_('svn: revision %s is not an integer') % rev)
328 raise util.Abort(_('svn: revision %s is not an integer') % rev)
329
329
330 self.trunkname = self.ui.config('convert', 'svn.trunk',
330 self.trunkname = self.ui.config('convert', 'svn.trunk',
331 'trunk').strip('/')
331 'trunk').strip('/')
332 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
332 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
333 try:
333 try:
334 self.startrev = int(self.startrev)
334 self.startrev = int(self.startrev)
335 if self.startrev < 0:
335 if self.startrev < 0:
336 self.startrev = 0
336 self.startrev = 0
337 except ValueError:
337 except ValueError:
338 raise util.Abort(_('svn: start revision %s is not an integer')
338 raise util.Abort(_('svn: start revision %s is not an integer')
339 % self.startrev)
339 % self.startrev)
340
340
341 try:
341 try:
342 self.head = self.latest(self.module, latest)
342 self.head = self.latest(self.module, latest)
343 except SvnPathNotFound:
343 except SvnPathNotFound:
344 self.head = None
344 self.head = None
345 if not self.head:
345 if not self.head:
346 raise util.Abort(_('no revision found in module %s')
346 raise util.Abort(_('no revision found in module %s')
347 % self.module)
347 % self.module)
348 self.last_changed = self.revnum(self.head)
348 self.last_changed = self.revnum(self.head)
349
349
350 self._changescache = None
350 self._changescache = None
351
351
352 if os.path.exists(os.path.join(url, '.svn/entries')):
352 if os.path.exists(os.path.join(url, '.svn/entries')):
353 self.wc = url
353 self.wc = url
354 else:
354 else:
355 self.wc = None
355 self.wc = None
356 self.convertfp = None
356 self.convertfp = None
357
357
358 def setrevmap(self, revmap):
358 def setrevmap(self, revmap):
359 lastrevs = {}
359 lastrevs = {}
360 for revid in revmap.iterkeys():
360 for revid in revmap.iterkeys():
361 uuid, module, revnum = revsplit(revid)
361 uuid, module, revnum = revsplit(revid)
362 lastrevnum = lastrevs.setdefault(module, revnum)
362 lastrevnum = lastrevs.setdefault(module, revnum)
363 if revnum > lastrevnum:
363 if revnum > lastrevnum:
364 lastrevs[module] = revnum
364 lastrevs[module] = revnum
365 self.lastrevs = lastrevs
365 self.lastrevs = lastrevs
366
366
367 def exists(self, path, optrev):
367 def exists(self, path, optrev):
368 try:
368 try:
369 svn.client.ls(self.url.rstrip('/') + '/' + quote(path),
369 svn.client.ls(self.url.rstrip('/') + '/' + quote(path),
370 optrev, False, self.ctx)
370 optrev, False, self.ctx)
371 return True
371 return True
372 except SubversionException:
372 except SubversionException:
373 return False
373 return False
374
374
375 def getheads(self):
375 def getheads(self):
376
376
377 def isdir(path, revnum):
377 def isdir(path, revnum):
378 kind = self._checkpath(path, revnum)
378 kind = self._checkpath(path, revnum)
379 return kind == svn.core.svn_node_dir
379 return kind == svn.core.svn_node_dir
380
380
381 def getcfgpath(name, rev):
381 def getcfgpath(name, rev):
382 cfgpath = self.ui.config('convert', 'svn.' + name)
382 cfgpath = self.ui.config('convert', 'svn.' + name)
383 if cfgpath is not None and cfgpath.strip() == '':
383 if cfgpath is not None and cfgpath.strip() == '':
384 return None
384 return None
385 path = (cfgpath or name).strip('/')
385 path = (cfgpath or name).strip('/')
386 if not self.exists(path, rev):
386 if not self.exists(path, rev):
387 if self.module.endswith(path) and name == 'trunk':
387 if self.module.endswith(path) and name == 'trunk':
388 # we are converting from inside this directory
388 # we are converting from inside this directory
389 return None
389 return None
390 if cfgpath:
390 if cfgpath:
391 raise util.Abort(_('expected %s to be at %r, but not found')
391 raise util.Abort(_('expected %s to be at %r, but not found')
392 % (name, path))
392 % (name, path))
393 return None
393 return None
394 self.ui.note(_('found %s at %r\n') % (name, path))
394 self.ui.note(_('found %s at %r\n') % (name, path))
395 return path
395 return path
396
396
397 rev = optrev(self.last_changed)
397 rev = optrev(self.last_changed)
398 oldmodule = ''
398 oldmodule = ''
399 trunk = getcfgpath('trunk', rev)
399 trunk = getcfgpath('trunk', rev)
400 self.tags = getcfgpath('tags', rev)
400 self.tags = getcfgpath('tags', rev)
401 branches = getcfgpath('branches', rev)
401 branches = getcfgpath('branches', rev)
402
402
403 # If the project has a trunk or branches, we will extract heads
403 # If the project has a trunk or branches, we will extract heads
404 # from them. We keep the project root otherwise.
404 # from them. We keep the project root otherwise.
405 if trunk:
405 if trunk:
406 oldmodule = self.module or ''
406 oldmodule = self.module or ''
407 self.module += '/' + trunk
407 self.module += '/' + trunk
408 self.head = self.latest(self.module, self.last_changed)
408 self.head = self.latest(self.module, self.last_changed)
409 if not self.head:
409 if not self.head:
410 raise util.Abort(_('no revision found in module %s')
410 raise util.Abort(_('no revision found in module %s')
411 % self.module)
411 % self.module)
412
412
413 # First head in the list is the module's head
413 # First head in the list is the module's head
414 self.heads = [self.head]
414 self.heads = [self.head]
415 if self.tags is not None:
415 if self.tags is not None:
416 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
416 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
417
417
418 # Check if branches bring a few more heads to the list
418 # Check if branches bring a few more heads to the list
419 if branches:
419 if branches:
420 rpath = self.url.strip('/')
420 rpath = self.url.strip('/')
421 branchnames = svn.client.ls(rpath + '/' + quote(branches),
421 branchnames = svn.client.ls(rpath + '/' + quote(branches),
422 rev, False, self.ctx)
422 rev, False, self.ctx)
423 for branch in sorted(branchnames):
423 for branch in sorted(branchnames):
424 module = '%s/%s/%s' % (oldmodule, branches, branch)
424 module = '%s/%s/%s' % (oldmodule, branches, branch)
425 if not isdir(module, self.last_changed):
425 if not isdir(module, self.last_changed):
426 continue
426 continue
427 brevid = self.latest(module, self.last_changed)
427 brevid = self.latest(module, self.last_changed)
428 if not brevid:
428 if not brevid:
429 self.ui.note(_('ignoring empty branch %s\n') % branch)
429 self.ui.note(_('ignoring empty branch %s\n') % branch)
430 continue
430 continue
431 self.ui.note(_('found branch %s at %d\n') %
431 self.ui.note(_('found branch %s at %d\n') %
432 (branch, self.revnum(brevid)))
432 (branch, self.revnum(brevid)))
433 self.heads.append(brevid)
433 self.heads.append(brevid)
434
434
435 if self.startrev and self.heads:
435 if self.startrev and self.heads:
436 if len(self.heads) > 1:
436 if len(self.heads) > 1:
437 raise util.Abort(_('svn: start revision is not supported '
437 raise util.Abort(_('svn: start revision is not supported '
438 'with more than one branch'))
438 'with more than one branch'))
439 revnum = self.revnum(self.heads[0])
439 revnum = self.revnum(self.heads[0])
440 if revnum < self.startrev:
440 if revnum < self.startrev:
441 raise util.Abort(
441 raise util.Abort(
442 _('svn: no revision found after start revision %d')
442 _('svn: no revision found after start revision %d')
443 % self.startrev)
443 % self.startrev)
444
444
445 return self.heads
445 return self.heads
446
446
447 def getchanges(self, rev):
447 def getchanges(self, rev):
448 if self._changescache and self._changescache[0] == rev:
448 if self._changescache and self._changescache[0] == rev:
449 return self._changescache[1]
449 return self._changescache[1]
450 self._changescache = None
450 self._changescache = None
451 (paths, parents) = self.paths[rev]
451 (paths, parents) = self.paths[rev]
452 if parents:
452 if parents:
453 files, self.removed, copies = self.expandpaths(rev, paths, parents)
453 files, self.removed, copies = self.expandpaths(rev, paths, parents)
454 else:
454 else:
455 # Perform a full checkout on roots
455 # Perform a full checkout on roots
456 uuid, module, revnum = revsplit(rev)
456 uuid, module, revnum = revsplit(rev)
457 entries = svn.client.ls(self.baseurl + quote(module),
457 entries = svn.client.ls(self.baseurl + quote(module),
458 optrev(revnum), True, self.ctx)
458 optrev(revnum), True, self.ctx)
459 files = [n for n, e in entries.iteritems()
459 files = [n for n, e in entries.iteritems()
460 if e.kind == svn.core.svn_node_file]
460 if e.kind == svn.core.svn_node_file]
461 copies = {}
461 copies = {}
462 self.removed = set()
462 self.removed = set()
463
463
464 files.sort()
464 files.sort()
465 files = zip(files, [rev] * len(files))
465 files = zip(files, [rev] * len(files))
466
466
467 # caller caches the result, so free it here to release memory
467 # caller caches the result, so free it here to release memory
468 del self.paths[rev]
468 del self.paths[rev]
469 return (files, copies)
469 return (files, copies)
470
470
471 def getchangedfiles(self, rev, i):
471 def getchangedfiles(self, rev, i):
472 changes = self.getchanges(rev)
472 changes = self.getchanges(rev)
473 self._changescache = (rev, changes)
473 self._changescache = (rev, changes)
474 return [f[0] for f in changes[0]]
474 return [f[0] for f in changes[0]]
475
475
476 def getcommit(self, rev):
476 def getcommit(self, rev):
477 if rev not in self.commits:
477 if rev not in self.commits:
478 uuid, module, revnum = revsplit(rev)
478 uuid, module, revnum = revsplit(rev)
479 self.module = module
479 self.module = module
480 self.reparent(module)
480 self.reparent(module)
481 # We assume that:
481 # We assume that:
482 # - requests for revisions after "stop" come from the
482 # - requests for revisions after "stop" come from the
483 # revision graph backward traversal. Cache all of them
483 # revision graph backward traversal. Cache all of them
484 # down to stop, they will be used eventually.
484 # down to stop, they will be used eventually.
485 # - requests for revisions before "stop" come to get
485 # - requests for revisions before "stop" come to get
486 # isolated branches parents. Just fetch what is needed.
486 # isolated branches parents. Just fetch what is needed.
487 stop = self.lastrevs.get(module, 0)
487 stop = self.lastrevs.get(module, 0)
488 if revnum < stop:
488 if revnum < stop:
489 stop = revnum + 1
489 stop = revnum + 1
490 self._fetch_revisions(revnum, stop)
490 self._fetch_revisions(revnum, stop)
491 if rev not in self.commits:
491 if rev not in self.commits:
492 raise util.Abort(_('svn: revision %s not found') % revnum)
492 raise util.Abort(_('svn: revision %s not found') % revnum)
493 commit = self.commits[rev]
493 commit = self.commits[rev]
494 # caller caches the result, so free it here to release memory
494 # caller caches the result, so free it here to release memory
495 del self.commits[rev]
495 del self.commits[rev]
496 return commit
496 return commit
497
497
498 def checkrevformat(self, revstr, mapname='splicemap'):
498 def checkrevformat(self, revstr, mapname='splicemap'):
499 """ fails if revision format does not match the correct format"""
499 """ fails if revision format does not match the correct format"""
500 if not re.match(r'svn:[0-9a-f]{8,8}-[0-9a-f]{4,4}-'
500 if not re.match(r'svn:[0-9a-f]{8,8}-[0-9a-f]{4,4}-'
501 '[0-9a-f]{4,4}-[0-9a-f]{4,4}-[0-9a-f]'
501 '[0-9a-f]{4,4}-[0-9a-f]{4,4}-[0-9a-f]'
502 '{12,12}(.*)\@[0-9]+$',revstr):
502 '{12,12}(.*)\@[0-9]+$',revstr):
503 raise util.Abort(_('%s entry %s is not a valid revision'
503 raise util.Abort(_('%s entry %s is not a valid revision'
504 ' identifier') % (mapname, revstr))
504 ' identifier') % (mapname, revstr))
505
505
506 def gettags(self):
506 def gettags(self):
507 tags = {}
507 tags = {}
508 if self.tags is None:
508 if self.tags is None:
509 return tags
509 return tags
510
510
511 # svn tags are just a convention, project branches left in a
511 # svn tags are just a convention, project branches left in a
512 # 'tags' directory. There is no other relationship than
512 # 'tags' directory. There is no other relationship than
513 # ancestry, which is expensive to discover and makes them hard
513 # ancestry, which is expensive to discover and makes them hard
514 # to update incrementally. Worse, past revisions may be
514 # to update incrementally. Worse, past revisions may be
515 # referenced by tags far away in the future, requiring a deep
515 # referenced by tags far away in the future, requiring a deep
516 # history traversal on every calculation. Current code
516 # history traversal on every calculation. Current code
517 # performs a single backward traversal, tracking moves within
517 # performs a single backward traversal, tracking moves within
518 # the tags directory (tag renaming) and recording a new tag
518 # the tags directory (tag renaming) and recording a new tag
519 # everytime a project is copied from outside the tags
519 # everytime a project is copied from outside the tags
520 # directory. It also lists deleted tags, this behaviour may
520 # directory. It also lists deleted tags, this behaviour may
521 # change in the future.
521 # change in the future.
522 pendings = []
522 pendings = []
523 tagspath = self.tags
523 tagspath = self.tags
524 start = svn.ra.get_latest_revnum(self.ra)
524 start = svn.ra.get_latest_revnum(self.ra)
525 stream = self._getlog([self.tags], start, self.startrev)
525 stream = self._getlog([self.tags], start, self.startrev)
526 try:
526 try:
527 for entry in stream:
527 for entry in stream:
528 origpaths, revnum, author, date, message = entry
528 origpaths, revnum, author, date, message = entry
529 if not origpaths:
529 if not origpaths:
530 origpaths = []
530 origpaths = []
531 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
531 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
532 in origpaths.iteritems() if e.copyfrom_path]
532 in origpaths.iteritems() if e.copyfrom_path]
533 # Apply moves/copies from more specific to general
533 # Apply moves/copies from more specific to general
534 copies.sort(reverse=True)
534 copies.sort(reverse=True)
535
535
536 srctagspath = tagspath
536 srctagspath = tagspath
537 if copies and copies[-1][2] == tagspath:
537 if copies and copies[-1][2] == tagspath:
538 # Track tags directory moves
538 # Track tags directory moves
539 srctagspath = copies.pop()[0]
539 srctagspath = copies.pop()[0]
540
540
541 for source, sourcerev, dest in copies:
541 for source, sourcerev, dest in copies:
542 if not dest.startswith(tagspath + '/'):
542 if not dest.startswith(tagspath + '/'):
543 continue
543 continue
544 for tag in pendings:
544 for tag in pendings:
545 if tag[0].startswith(dest):
545 if tag[0].startswith(dest):
546 tagpath = source + tag[0][len(dest):]
546 tagpath = source + tag[0][len(dest):]
547 tag[:2] = [tagpath, sourcerev]
547 tag[:2] = [tagpath, sourcerev]
548 break
548 break
549 else:
549 else:
550 pendings.append([source, sourcerev, dest])
550 pendings.append([source, sourcerev, dest])
551
551
552 # Filter out tags with children coming from different
552 # Filter out tags with children coming from different
553 # parts of the repository like:
553 # parts of the repository like:
554 # /tags/tag.1 (from /trunk:10)
554 # /tags/tag.1 (from /trunk:10)
555 # /tags/tag.1/foo (from /branches/foo:12)
555 # /tags/tag.1/foo (from /branches/foo:12)
556 # Here/tags/tag.1 discarded as well as its children.
556 # Here/tags/tag.1 discarded as well as its children.
557 # It happens with tools like cvs2svn. Such tags cannot
557 # It happens with tools like cvs2svn. Such tags cannot
558 # be represented in mercurial.
558 # be represented in mercurial.
559 addeds = dict((p, e.copyfrom_path) for p, e
559 addeds = dict((p, e.copyfrom_path) for p, e
560 in origpaths.iteritems()
560 in origpaths.iteritems()
561 if e.action == 'A' and e.copyfrom_path)
561 if e.action == 'A' and e.copyfrom_path)
562 badroots = set()
562 badroots = set()
563 for destroot in addeds:
563 for destroot in addeds:
564 for source, sourcerev, dest in pendings:
564 for source, sourcerev, dest in pendings:
565 if (not dest.startswith(destroot + '/')
565 if (not dest.startswith(destroot + '/')
566 or source.startswith(addeds[destroot] + '/')):
566 or source.startswith(addeds[destroot] + '/')):
567 continue
567 continue
568 badroots.add(destroot)
568 badroots.add(destroot)
569 break
569 break
570
570
571 for badroot in badroots:
571 for badroot in badroots:
572 pendings = [p for p in pendings if p[2] != badroot
572 pendings = [p for p in pendings if p[2] != badroot
573 and not p[2].startswith(badroot + '/')]
573 and not p[2].startswith(badroot + '/')]
574
574
575 # Tell tag renamings from tag creations
575 # Tell tag renamings from tag creations
576 renamings = []
576 renamings = []
577 for source, sourcerev, dest in pendings:
577 for source, sourcerev, dest in pendings:
578 tagname = dest.split('/')[-1]
578 tagname = dest.split('/')[-1]
579 if source.startswith(srctagspath):
579 if source.startswith(srctagspath):
580 renamings.append([source, sourcerev, tagname])
580 renamings.append([source, sourcerev, tagname])
581 continue
581 continue
582 if tagname in tags:
582 if tagname in tags:
583 # Keep the latest tag value
583 # Keep the latest tag value
584 continue
584 continue
585 # From revision may be fake, get one with changes
585 # From revision may be fake, get one with changes
586 try:
586 try:
587 tagid = self.latest(source, sourcerev)
587 tagid = self.latest(source, sourcerev)
588 if tagid and tagname not in tags:
588 if tagid and tagname not in tags:
589 tags[tagname] = tagid
589 tags[tagname] = tagid
590 except SvnPathNotFound:
590 except SvnPathNotFound:
591 # It happens when we are following directories
591 # It happens when we are following directories
592 # we assumed were copied with their parents
592 # we assumed were copied with their parents
593 # but were really created in the tag
593 # but were really created in the tag
594 # directory.
594 # directory.
595 pass
595 pass
596 pendings = renamings
596 pendings = renamings
597 tagspath = srctagspath
597 tagspath = srctagspath
598 finally:
598 finally:
599 stream.close()
599 stream.close()
600 return tags
600 return tags
601
601
602 def converted(self, rev, destrev):
602 def converted(self, rev, destrev):
603 if not self.wc:
603 if not self.wc:
604 return
604 return
605 if self.convertfp is None:
605 if self.convertfp is None:
606 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
606 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
607 'a')
607 'a')
608 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
608 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
609 self.convertfp.flush()
609 self.convertfp.flush()
610
610
611 def revid(self, revnum, module=None):
611 def revid(self, revnum, module=None):
612 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
612 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
613
613
614 def revnum(self, rev):
614 def revnum(self, rev):
615 return int(rev.split('@')[-1])
615 return int(rev.split('@')[-1])
616
616
617 def latest(self, path, stop=None):
617 def latest(self, path, stop=None):
618 """Find the latest revid affecting path, up to stop revision
618 """Find the latest revid affecting path, up to stop revision
619 number. If stop is None, default to repository latest
619 number. If stop is None, default to repository latest
620 revision. It may return a revision in a different module,
620 revision. It may return a revision in a different module,
621 since a branch may be moved without a change being
621 since a branch may be moved without a change being
622 reported. Return None if computed module does not belong to
622 reported. Return None if computed module does not belong to
623 rootmodule subtree.
623 rootmodule subtree.
624 """
624 """
625 def findchanges(path, start, stop=None):
625 def findchanges(path, start, stop=None):
626 stream = self._getlog([path], start, stop or 1)
626 stream = self._getlog([path], start, stop or 1)
627 try:
627 try:
628 for entry in stream:
628 for entry in stream:
629 paths, revnum, author, date, message = entry
629 paths, revnum, author, date, message = entry
630 if stop is None and paths:
630 if stop is None and paths:
631 # We do not know the latest changed revision,
631 # We do not know the latest changed revision,
632 # keep the first one with changed paths.
632 # keep the first one with changed paths.
633 break
633 break
634 if revnum <= stop:
634 if revnum <= stop:
635 break
635 break
636
636
637 for p in paths:
637 for p in paths:
638 if (not path.startswith(p) or
638 if (not path.startswith(p) or
639 not paths[p].copyfrom_path):
639 not paths[p].copyfrom_path):
640 continue
640 continue
641 newpath = paths[p].copyfrom_path + path[len(p):]
641 newpath = paths[p].copyfrom_path + path[len(p):]
642 self.ui.debug("branch renamed from %s to %s at %d\n" %
642 self.ui.debug("branch renamed from %s to %s at %d\n" %
643 (path, newpath, revnum))
643 (path, newpath, revnum))
644 path = newpath
644 path = newpath
645 break
645 break
646 if not paths:
646 if not paths:
647 revnum = None
647 revnum = None
648 return revnum, path
648 return revnum, path
649 finally:
649 finally:
650 stream.close()
650 stream.close()
651
651
652 if not path.startswith(self.rootmodule):
652 if not path.startswith(self.rootmodule):
653 # Requests on foreign branches may be forbidden at server level
653 # Requests on foreign branches may be forbidden at server level
654 self.ui.debug('ignoring foreign branch %r\n' % path)
654 self.ui.debug('ignoring foreign branch %r\n' % path)
655 return None
655 return None
656
656
657 if stop is None:
657 if stop is None:
658 stop = svn.ra.get_latest_revnum(self.ra)
658 stop = svn.ra.get_latest_revnum(self.ra)
659 try:
659 try:
660 prevmodule = self.reparent('')
660 prevmodule = self.reparent('')
661 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
661 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
662 self.reparent(prevmodule)
662 self.reparent(prevmodule)
663 except SubversionException:
663 except SubversionException:
664 dirent = None
664 dirent = None
665 if not dirent:
665 if not dirent:
666 raise SvnPathNotFound(_('%s not found up to revision %d')
666 raise SvnPathNotFound(_('%s not found up to revision %d')
667 % (path, stop))
667 % (path, stop))
668
668
669 # stat() gives us the previous revision on this line of
669 # stat() gives us the previous revision on this line of
670 # development, but it might be in *another module*. Fetch the
670 # development, but it might be in *another module*. Fetch the
671 # log and detect renames down to the latest revision.
671 # log and detect renames down to the latest revision.
672 revnum, realpath = findchanges(path, stop, dirent.created_rev)
672 revnum, realpath = findchanges(path, stop, dirent.created_rev)
673 if revnum is None:
673 if revnum is None:
674 # Tools like svnsync can create empty revision, when
674 # Tools like svnsync can create empty revision, when
675 # synchronizing only a subtree for instance. These empty
675 # synchronizing only a subtree for instance. These empty
676 # revisions created_rev still have their original values
676 # revisions created_rev still have their original values
677 # despite all changes having disappeared and can be
677 # despite all changes having disappeared and can be
678 # returned by ra.stat(), at least when stating the root
678 # returned by ra.stat(), at least when stating the root
679 # module. In that case, do not trust created_rev and scan
679 # module. In that case, do not trust created_rev and scan
680 # the whole history.
680 # the whole history.
681 revnum, realpath = findchanges(path, stop)
681 revnum, realpath = findchanges(path, stop)
682 if revnum is None:
682 if revnum is None:
683 self.ui.debug('ignoring empty branch %r\n' % realpath)
683 self.ui.debug('ignoring empty branch %r\n' % realpath)
684 return None
684 return None
685
685
686 if not realpath.startswith(self.rootmodule):
686 if not realpath.startswith(self.rootmodule):
687 self.ui.debug('ignoring foreign branch %r\n' % realpath)
687 self.ui.debug('ignoring foreign branch %r\n' % realpath)
688 return None
688 return None
689 return self.revid(revnum, realpath)
689 return self.revid(revnum, realpath)
690
690
691 def reparent(self, module):
691 def reparent(self, module):
692 """Reparent the svn transport and return the previous parent."""
692 """Reparent the svn transport and return the previous parent."""
693 if self.prevmodule == module:
693 if self.prevmodule == module:
694 return module
694 return module
695 svnurl = self.baseurl + quote(module)
695 svnurl = self.baseurl + quote(module)
696 prevmodule = self.prevmodule
696 prevmodule = self.prevmodule
697 if prevmodule is None:
697 if prevmodule is None:
698 prevmodule = ''
698 prevmodule = ''
699 self.ui.debug("reparent to %s\n" % svnurl)
699 self.ui.debug("reparent to %s\n" % svnurl)
700 svn.ra.reparent(self.ra, svnurl)
700 svn.ra.reparent(self.ra, svnurl)
701 self.prevmodule = module
701 self.prevmodule = module
702 return prevmodule
702 return prevmodule
703
703
704 def expandpaths(self, rev, paths, parents):
704 def expandpaths(self, rev, paths, parents):
705 changed, removed = set(), set()
705 changed, removed = set(), set()
706 copies = {}
706 copies = {}
707
707
708 new_module, revnum = revsplit(rev)[1:]
708 new_module, revnum = revsplit(rev)[1:]
709 if new_module != self.module:
709 if new_module != self.module:
710 self.module = new_module
710 self.module = new_module
711 self.reparent(self.module)
711 self.reparent(self.module)
712
712
713 for i, (path, ent) in enumerate(paths):
713 for i, (path, ent) in enumerate(paths):
714 self.ui.progress(_('scanning paths'), i, item=path,
714 self.ui.progress(_('scanning paths'), i, item=path,
715 total=len(paths))
715 total=len(paths))
716 entrypath = self.getrelpath(path)
716 entrypath = self.getrelpath(path)
717
717
718 kind = self._checkpath(entrypath, revnum)
718 kind = self._checkpath(entrypath, revnum)
719 if kind == svn.core.svn_node_file:
719 if kind == svn.core.svn_node_file:
720 changed.add(self.recode(entrypath))
720 changed.add(self.recode(entrypath))
721 if not ent.copyfrom_path or not parents:
721 if not ent.copyfrom_path or not parents:
722 continue
722 continue
723 # Copy sources not in parent revisions cannot be
723 # Copy sources not in parent revisions cannot be
724 # represented, ignore their origin for now
724 # represented, ignore their origin for now
725 pmodule, prevnum = revsplit(parents[0])[1:]
725 pmodule, prevnum = revsplit(parents[0])[1:]
726 if ent.copyfrom_rev < prevnum:
726 if ent.copyfrom_rev < prevnum:
727 continue
727 continue
728 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
728 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
729 if not copyfrom_path:
729 if not copyfrom_path:
730 continue
730 continue
731 self.ui.debug("copied to %s from %s@%s\n" %
731 self.ui.debug("copied to %s from %s@%s\n" %
732 (entrypath, copyfrom_path, ent.copyfrom_rev))
732 (entrypath, copyfrom_path, ent.copyfrom_rev))
733 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
733 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
734 elif kind == 0: # gone, but had better be a deleted *file*
734 elif kind == 0: # gone, but had better be a deleted *file*
735 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
735 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
736 pmodule, prevnum = revsplit(parents[0])[1:]
736 pmodule, prevnum = revsplit(parents[0])[1:]
737 parentpath = pmodule + "/" + entrypath
737 parentpath = pmodule + "/" + entrypath
738 fromkind = self._checkpath(entrypath, prevnum, pmodule)
738 fromkind = self._checkpath(entrypath, prevnum, pmodule)
739
739
740 if fromkind == svn.core.svn_node_file:
740 if fromkind == svn.core.svn_node_file:
741 removed.add(self.recode(entrypath))
741 removed.add(self.recode(entrypath))
742 elif fromkind == svn.core.svn_node_dir:
742 elif fromkind == svn.core.svn_node_dir:
743 oroot = parentpath.strip('/')
743 oroot = parentpath.strip('/')
744 nroot = path.strip('/')
744 nroot = path.strip('/')
745 children = self._iterfiles(oroot, prevnum)
745 children = self._iterfiles(oroot, prevnum)
746 for childpath in children:
746 for childpath in children:
747 childpath = childpath.replace(oroot, nroot)
747 childpath = childpath.replace(oroot, nroot)
748 childpath = self.getrelpath("/" + childpath, pmodule)
748 childpath = self.getrelpath("/" + childpath, pmodule)
749 if childpath:
749 if childpath:
750 removed.add(self.recode(childpath))
750 removed.add(self.recode(childpath))
751 else:
751 else:
752 self.ui.debug('unknown path in revision %d: %s\n' % \
752 self.ui.debug('unknown path in revision %d: %s\n' % \
753 (revnum, path))
753 (revnum, path))
754 elif kind == svn.core.svn_node_dir:
754 elif kind == svn.core.svn_node_dir:
755 if ent.action == 'M':
755 if ent.action == 'M':
756 # If the directory just had a prop change,
756 # If the directory just had a prop change,
757 # then we shouldn't need to look for its children.
757 # then we shouldn't need to look for its children.
758 continue
758 continue
759 if ent.action == 'R' and parents:
759 if ent.action == 'R' and parents:
760 # If a directory is replacing a file, mark the previous
760 # If a directory is replacing a file, mark the previous
761 # file as deleted
761 # file as deleted
762 pmodule, prevnum = revsplit(parents[0])[1:]
762 pmodule, prevnum = revsplit(parents[0])[1:]
763 pkind = self._checkpath(entrypath, prevnum, pmodule)
763 pkind = self._checkpath(entrypath, prevnum, pmodule)
764 if pkind == svn.core.svn_node_file:
764 if pkind == svn.core.svn_node_file:
765 removed.add(self.recode(entrypath))
765 removed.add(self.recode(entrypath))
766 elif pkind == svn.core.svn_node_dir:
766 elif pkind == svn.core.svn_node_dir:
767 # We do not know what files were kept or removed,
767 # We do not know what files were kept or removed,
768 # mark them all as changed.
768 # mark them all as changed.
769 for childpath in self._iterfiles(pmodule, prevnum):
769 for childpath in self._iterfiles(pmodule, prevnum):
770 childpath = self.getrelpath("/" + childpath)
770 childpath = self.getrelpath("/" + childpath)
771 if childpath:
771 if childpath:
772 changed.add(self.recode(childpath))
772 changed.add(self.recode(childpath))
773
773
774 for childpath in self._iterfiles(path, revnum):
774 for childpath in self._iterfiles(path, revnum):
775 childpath = self.getrelpath("/" + childpath)
775 childpath = self.getrelpath("/" + childpath)
776 if childpath:
776 if childpath:
777 changed.add(self.recode(childpath))
777 changed.add(self.recode(childpath))
778
778
779 # Handle directory copies
779 # Handle directory copies
780 if not ent.copyfrom_path or not parents:
780 if not ent.copyfrom_path or not parents:
781 continue
781 continue
782 # Copy sources not in parent revisions cannot be
782 # Copy sources not in parent revisions cannot be
783 # represented, ignore their origin for now
783 # represented, ignore their origin for now
784 pmodule, prevnum = revsplit(parents[0])[1:]
784 pmodule, prevnum = revsplit(parents[0])[1:]
785 if ent.copyfrom_rev < prevnum:
785 if ent.copyfrom_rev < prevnum:
786 continue
786 continue
787 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
787 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
788 if not copyfrompath:
788 if not copyfrompath:
789 continue
789 continue
790 self.ui.debug("mark %s came from %s:%d\n"
790 self.ui.debug("mark %s came from %s:%d\n"
791 % (path, copyfrompath, ent.copyfrom_rev))
791 % (path, copyfrompath, ent.copyfrom_rev))
792 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
792 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
793 for childpath in children:
793 for childpath in children:
794 childpath = self.getrelpath("/" + childpath, pmodule)
794 childpath = self.getrelpath("/" + childpath, pmodule)
795 if not childpath:
795 if not childpath:
796 continue
796 continue
797 copytopath = path + childpath[len(copyfrompath):]
797 copytopath = path + childpath[len(copyfrompath):]
798 copytopath = self.getrelpath(copytopath)
798 copytopath = self.getrelpath(copytopath)
799 copies[self.recode(copytopath)] = self.recode(childpath)
799 copies[self.recode(copytopath)] = self.recode(childpath)
800
800
801 self.ui.progress(_('scanning paths'), None)
801 self.ui.progress(_('scanning paths'), None)
802 changed.update(removed)
802 changed.update(removed)
803 return (list(changed), removed, copies)
803 return (list(changed), removed, copies)
804
804
805 def _fetch_revisions(self, from_revnum, to_revnum):
805 def _fetch_revisions(self, from_revnum, to_revnum):
806 if from_revnum < to_revnum:
806 if from_revnum < to_revnum:
807 from_revnum, to_revnum = to_revnum, from_revnum
807 from_revnum, to_revnum = to_revnum, from_revnum
808
808
809 self.child_cset = None
809 self.child_cset = None
810
810
811 def parselogentry(orig_paths, revnum, author, date, message):
811 def parselogentry(orig_paths, revnum, author, date, message):
812 """Return the parsed commit object or None, and True if
812 """Return the parsed commit object or None, and True if
813 the revision is a branch root.
813 the revision is a branch root.
814 """
814 """
815 self.ui.debug("parsing revision %d (%d changes)\n" %
815 self.ui.debug("parsing revision %d (%d changes)\n" %
816 (revnum, len(orig_paths)))
816 (revnum, len(orig_paths)))
817
817
818 branched = False
818 branched = False
819 rev = self.revid(revnum)
819 rev = self.revid(revnum)
820 # branch log might return entries for a parent we already have
820 # branch log might return entries for a parent we already have
821
821
822 if rev in self.commits or revnum < to_revnum:
822 if rev in self.commits or revnum < to_revnum:
823 return None, branched
823 return None, branched
824
824
825 parents = []
825 parents = []
826 # check whether this revision is the start of a branch or part
826 # check whether this revision is the start of a branch or part
827 # of a branch renaming
827 # of a branch renaming
828 orig_paths = sorted(orig_paths.iteritems())
828 orig_paths = sorted(orig_paths.iteritems())
829 root_paths = [(p, e) for p, e in orig_paths
829 root_paths = [(p, e) for p, e in orig_paths
830 if self.module.startswith(p)]
830 if self.module.startswith(p)]
831 if root_paths:
831 if root_paths:
832 path, ent = root_paths[-1]
832 path, ent = root_paths[-1]
833 if ent.copyfrom_path:
833 if ent.copyfrom_path:
834 branched = True
834 branched = True
835 newpath = ent.copyfrom_path + self.module[len(path):]
835 newpath = ent.copyfrom_path + self.module[len(path):]
836 # ent.copyfrom_rev may not be the actual last revision
836 # ent.copyfrom_rev may not be the actual last revision
837 previd = self.latest(newpath, ent.copyfrom_rev)
837 previd = self.latest(newpath, ent.copyfrom_rev)
838 if previd is not None:
838 if previd is not None:
839 prevmodule, prevnum = revsplit(previd)[1:]
839 prevmodule, prevnum = revsplit(previd)[1:]
840 if prevnum >= self.startrev:
840 if prevnum >= self.startrev:
841 parents = [previd]
841 parents = [previd]
842 self.ui.note(
842 self.ui.note(
843 _('found parent of branch %s at %d: %s\n') %
843 _('found parent of branch %s at %d: %s\n') %
844 (self.module, prevnum, prevmodule))
844 (self.module, prevnum, prevmodule))
845 else:
845 else:
846 self.ui.debug("no copyfrom path, don't know what to do.\n")
846 self.ui.debug("no copyfrom path, don't know what to do.\n")
847
847
848 paths = []
848 paths = []
849 # filter out unrelated paths
849 # filter out unrelated paths
850 for path, ent in orig_paths:
850 for path, ent in orig_paths:
851 if self.getrelpath(path) is None:
851 if self.getrelpath(path) is None:
852 continue
852 continue
853 paths.append((path, ent))
853 paths.append((path, ent))
854
854
855 # Example SVN datetime. Includes microseconds.
855 # Example SVN datetime. Includes microseconds.
856 # ISO-8601 conformant
856 # ISO-8601 conformant
857 # '2007-01-04T17:35:00.902377Z'
857 # '2007-01-04T17:35:00.902377Z'
858 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
858 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
859 if self.ui.configbool('convert', 'localtimezone'):
859 if self.ui.configbool('convert', 'localtimezone'):
860 date = makedatetimestamp(date[0])
860 date = makedatetimestamp(date[0])
861
861
862 log = message and self.recode(message) or ''
862 log = message and self.recode(message) or ''
863 author = author and self.recode(author) or ''
863 author = author and self.recode(author) or ''
864 try:
864 try:
865 branch = self.module.split("/")[-1]
865 branch = self.module.split("/")[-1]
866 if branch == self.trunkname:
866 if branch == self.trunkname:
867 branch = None
867 branch = None
868 except IndexError:
868 except IndexError:
869 branch = None
869 branch = None
870
870
871 cset = commit(author=author,
871 cset = commit(author=author,
872 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
872 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
873 desc=log,
873 desc=log,
874 parents=parents,
874 parents=parents,
875 branch=branch,
875 branch=branch,
876 rev=rev)
876 rev=rev)
877
877
878 self.commits[rev] = cset
878 self.commits[rev] = cset
879 # The parents list is *shared* among self.paths and the
879 # The parents list is *shared* among self.paths and the
880 # commit object. Both will be updated below.
880 # commit object. Both will be updated below.
881 self.paths[rev] = (paths, cset.parents)
881 self.paths[rev] = (paths, cset.parents)
882 if self.child_cset and not self.child_cset.parents:
882 if self.child_cset and not self.child_cset.parents:
883 self.child_cset.parents[:] = [rev]
883 self.child_cset.parents[:] = [rev]
884 self.child_cset = cset
884 self.child_cset = cset
885 return cset, branched
885 return cset, branched
886
886
887 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
887 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
888 (self.module, from_revnum, to_revnum))
888 (self.module, from_revnum, to_revnum))
889
889
890 try:
890 try:
891 firstcset = None
891 firstcset = None
892 lastonbranch = False
892 lastonbranch = False
893 stream = self._getlog([self.module], from_revnum, to_revnum)
893 stream = self._getlog([self.module], from_revnum, to_revnum)
894 try:
894 try:
895 for entry in stream:
895 for entry in stream:
896 paths, revnum, author, date, message = entry
896 paths, revnum, author, date, message = entry
897 if revnum < self.startrev:
897 if revnum < self.startrev:
898 lastonbranch = True
898 lastonbranch = True
899 break
899 break
900 if not paths:
900 if not paths:
901 self.ui.debug('revision %d has no entries\n' % revnum)
901 self.ui.debug('revision %d has no entries\n' % revnum)
902 # If we ever leave the loop on an empty
902 # If we ever leave the loop on an empty
903 # revision, do not try to get a parent branch
903 # revision, do not try to get a parent branch
904 lastonbranch = lastonbranch or revnum == 0
904 lastonbranch = lastonbranch or revnum == 0
905 continue
905 continue
906 cset, lastonbranch = parselogentry(paths, revnum, author,
906 cset, lastonbranch = parselogentry(paths, revnum, author,
907 date, message)
907 date, message)
908 if cset:
908 if cset:
909 firstcset = cset
909 firstcset = cset
910 if lastonbranch:
910 if lastonbranch:
911 break
911 break
912 finally:
912 finally:
913 stream.close()
913 stream.close()
914
914
915 if not lastonbranch and firstcset and not firstcset.parents:
915 if not lastonbranch and firstcset and not firstcset.parents:
916 # The first revision of the sequence (the last fetched one)
916 # The first revision of the sequence (the last fetched one)
917 # has invalid parents if not a branch root. Find the parent
917 # has invalid parents if not a branch root. Find the parent
918 # revision now, if any.
918 # revision now, if any.
919 try:
919 try:
920 firstrevnum = self.revnum(firstcset.rev)
920 firstrevnum = self.revnum(firstcset.rev)
921 if firstrevnum > 1:
921 if firstrevnum > 1:
922 latest = self.latest(self.module, firstrevnum - 1)
922 latest = self.latest(self.module, firstrevnum - 1)
923 if latest:
923 if latest:
924 firstcset.parents.append(latest)
924 firstcset.parents.append(latest)
925 except SvnPathNotFound:
925 except SvnPathNotFound:
926 pass
926 pass
927 except SubversionException, (inst, num):
927 except SubversionException, (inst, num):
928 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
928 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
929 raise util.Abort(_('svn: branch has no revision %s')
929 raise util.Abort(_('svn: branch has no revision %s')
930 % to_revnum)
930 % to_revnum)
931 raise
931 raise
932
932
933 def getfile(self, file, rev):
933 def getfile(self, file, rev):
934 # TODO: ra.get_file transmits the whole file instead of diffs.
934 # TODO: ra.get_file transmits the whole file instead of diffs.
935 if file in self.removed:
935 if file in self.removed:
936 raise IOError
936 raise IOError
937 mode = ''
937 mode = ''
938 try:
938 try:
939 new_module, revnum = revsplit(rev)[1:]
939 new_module, revnum = revsplit(rev)[1:]
940 if self.module != new_module:
940 if self.module != new_module:
941 self.module = new_module
941 self.module = new_module
942 self.reparent(self.module)
942 self.reparent(self.module)
943 io = StringIO()
943 io = StringIO()
944 info = svn.ra.get_file(self.ra, file, revnum, io)
944 info = svn.ra.get_file(self.ra, file, revnum, io)
945 data = io.getvalue()
945 data = io.getvalue()
946 # ra.get_file() seems to keep a reference on the input buffer
946 # ra.get_file() seems to keep a reference on the input buffer
947 # preventing collection. Release it explicitly.
947 # preventing collection. Release it explicitly.
948 io.close()
948 io.close()
949 if isinstance(info, list):
949 if isinstance(info, list):
950 info = info[-1]
950 info = info[-1]
951 mode = ("svn:executable" in info) and 'x' or ''
951 mode = ("svn:executable" in info) and 'x' or ''
952 mode = ("svn:special" in info) and 'l' or mode
952 mode = ("svn:special" in info) and 'l' or mode
953 except SubversionException, e:
953 except SubversionException, e:
954 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
954 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
955 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
955 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
956 if e.apr_err in notfound: # File not found
956 if e.apr_err in notfound: # File not found
957 raise IOError
957 raise IOError
958 raise
958 raise
959 if mode == 'l':
959 if mode == 'l':
960 link_prefix = "link "
960 link_prefix = "link "
961 if data.startswith(link_prefix):
961 if data.startswith(link_prefix):
962 data = data[len(link_prefix):]
962 data = data[len(link_prefix):]
963 return data, mode
963 return data, mode
964
964
965 def _iterfiles(self, path, revnum):
965 def _iterfiles(self, path, revnum):
966 """Enumerate all files in path at revnum, recursively."""
966 """Enumerate all files in path at revnum, recursively."""
967 path = path.strip('/')
967 path = path.strip('/')
968 pool = Pool()
968 pool = Pool()
969 rpath = '/'.join([self.baseurl, quote(path)]).strip('/')
969 rpath = '/'.join([self.baseurl, quote(path)]).strip('/')
970 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
970 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
971 if path:
971 if path:
972 path += '/'
972 path += '/'
973 return ((path + p) for p, e in entries.iteritems()
973 return ((path + p) for p, e in entries.iteritems()
974 if e.kind == svn.core.svn_node_file)
974 if e.kind == svn.core.svn_node_file)
975
975
976 def getrelpath(self, path, module=None):
976 def getrelpath(self, path, module=None):
977 if module is None:
977 if module is None:
978 module = self.module
978 module = self.module
979 # Given the repository url of this wc, say
979 # Given the repository url of this wc, say
980 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
980 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
981 # extract the "entry" portion (a relative path) from what
981 # extract the "entry" portion (a relative path) from what
982 # svn log --xml says, i.e.
982 # svn log --xml says, i.e.
983 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
983 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
984 # that is to say "tests/PloneTestCase.py"
984 # that is to say "tests/PloneTestCase.py"
985 if path.startswith(module):
985 if path.startswith(module):
986 relative = path.rstrip('/')[len(module):]
986 relative = path.rstrip('/')[len(module):]
987 if relative.startswith('/'):
987 if relative.startswith('/'):
988 return relative[1:]
988 return relative[1:]
989 elif relative == '':
989 elif relative == '':
990 return relative
990 return relative
991
991
992 # The path is outside our tracked tree...
992 # The path is outside our tracked tree...
993 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
993 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
994 return None
994 return None
995
995
996 def _checkpath(self, path, revnum, module=None):
996 def _checkpath(self, path, revnum, module=None):
997 if module is not None:
997 if module is not None:
998 prevmodule = self.reparent('')
998 prevmodule = self.reparent('')
999 path = module + '/' + path
999 path = module + '/' + path
1000 try:
1000 try:
1001 # ra.check_path does not like leading slashes very much, it leads
1001 # ra.check_path does not like leading slashes very much, it leads
1002 # to PROPFIND subversion errors
1002 # to PROPFIND subversion errors
1003 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
1003 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
1004 finally:
1004 finally:
1005 if module is not None:
1005 if module is not None:
1006 self.reparent(prevmodule)
1006 self.reparent(prevmodule)
1007
1007
1008 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
1008 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
1009 strict_node_history=False):
1009 strict_node_history=False):
1010 # Normalize path names, svn >= 1.5 only wants paths relative to
1010 # Normalize path names, svn >= 1.5 only wants paths relative to
1011 # supplied URL
1011 # supplied URL
1012 relpaths = []
1012 relpaths = []
1013 for p in paths:
1013 for p in paths:
1014 if not p.startswith('/'):
1014 if not p.startswith('/'):
1015 p = self.module + '/' + p
1015 p = self.module + '/' + p
1016 relpaths.append(p.strip('/'))
1016 relpaths.append(p.strip('/'))
1017 args = [self.baseurl, relpaths, start, end, limit,
1017 args = [self.baseurl, relpaths, start, end, limit,
1018 discover_changed_paths, strict_node_history]
1018 discover_changed_paths, strict_node_history]
1019 # undocumented feature: debugsvnlog can be disabled
1019 # undocumented feature: debugsvnlog can be disabled
1020 if not self.ui.configbool('convert', 'svn.debugsvnlog', True):
1020 if not self.ui.configbool('convert', 'svn.debugsvnlog', True):
1021 return directlogstream(*args)
1021 return directlogstream(*args)
1022 arg = encodeargs(args)
1022 arg = encodeargs(args)
1023 hgexe = util.hgexecutable()
1023 hgexe = util.hgexecutable()
1024 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
1024 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
1025 stdin, stdout = util.popen2(util.quotecommand(cmd))
1025 stdin, stdout = util.popen2(util.quotecommand(cmd))
1026 stdin.write(arg)
1026 stdin.write(arg)
1027 try:
1027 try:
1028 stdin.close()
1028 stdin.close()
1029 except IOError:
1029 except IOError:
1030 raise util.Abort(_('Mercurial failed to run itself, check'
1030 raise util.Abort(_('Mercurial failed to run itself, check'
1031 ' hg executable is in PATH'))
1031 ' hg executable is in PATH'))
1032 return logstream(stdout)
1032 return logstream(stdout)
1033
1033
1034 pre_revprop_change = '''#!/bin/sh
1034 pre_revprop_change = '''#!/bin/sh
1035
1035
1036 REPOS="$1"
1036 REPOS="$1"
1037 REV="$2"
1037 REV="$2"
1038 USER="$3"
1038 USER="$3"
1039 PROPNAME="$4"
1039 PROPNAME="$4"
1040 ACTION="$5"
1040 ACTION="$5"
1041
1041
1042 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
1042 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
1043 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
1043 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
1044 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
1044 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
1045
1045
1046 echo "Changing prohibited revision property" >&2
1046 echo "Changing prohibited revision property" >&2
1047 exit 1
1047 exit 1
1048 '''
1048 '''
1049
1049
1050 class svn_sink(converter_sink, commandline):
1050 class svn_sink(converter_sink, commandline):
1051 commit_re = re.compile(r'Committed revision (\d+).', re.M)
1051 commit_re = re.compile(r'Committed revision (\d+).', re.M)
1052 uuid_re = re.compile(r'Repository UUID:\s*(\S+)', re.M)
1052 uuid_re = re.compile(r'Repository UUID:\s*(\S+)', re.M)
1053
1053
1054 def prerun(self):
1054 def prerun(self):
1055 if self.wc:
1055 if self.wc:
1056 os.chdir(self.wc)
1056 os.chdir(self.wc)
1057
1057
1058 def postrun(self):
1058 def postrun(self):
1059 if self.wc:
1059 if self.wc:
1060 os.chdir(self.cwd)
1060 os.chdir(self.cwd)
1061
1061
1062 def join(self, name):
1062 def join(self, name):
1063 return os.path.join(self.wc, '.svn', name)
1063 return os.path.join(self.wc, '.svn', name)
1064
1064
1065 def revmapfile(self):
1065 def revmapfile(self):
1066 return self.join('hg-shamap')
1066 return self.join('hg-shamap')
1067
1067
1068 def authorfile(self):
1068 def authorfile(self):
1069 return self.join('hg-authormap')
1069 return self.join('hg-authormap')
1070
1070
1071 def __init__(self, ui, path):
1071 def __init__(self, ui, path):
1072
1072
1073 converter_sink.__init__(self, ui, path)
1073 converter_sink.__init__(self, ui, path)
1074 commandline.__init__(self, ui, 'svn')
1074 commandline.__init__(self, ui, 'svn')
1075 self.delete = []
1075 self.delete = []
1076 self.setexec = []
1076 self.setexec = []
1077 self.delexec = []
1077 self.delexec = []
1078 self.copies = []
1078 self.copies = []
1079 self.wc = None
1079 self.wc = None
1080 self.cwd = os.getcwd()
1080 self.cwd = os.getcwd()
1081
1081
1082 created = False
1082 created = False
1083 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
1083 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
1084 self.wc = os.path.realpath(path)
1084 self.wc = os.path.realpath(path)
1085 self.run0('update')
1085 self.run0('update')
1086 else:
1086 else:
1087 if not re.search(r'^(file|http|https|svn|svn\+ssh)\://', path):
1087 if not re.search(r'^(file|http|https|svn|svn\+ssh)\://', path):
1088 path = os.path.realpath(path)
1088 path = os.path.realpath(path)
1089 if os.path.isdir(os.path.dirname(path)):
1089 if os.path.isdir(os.path.dirname(path)):
1090 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
1090 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
1091 ui.status(_('initializing svn repository %r\n') %
1091 ui.status(_('initializing svn repository %r\n') %
1092 os.path.basename(path))
1092 os.path.basename(path))
1093 commandline(ui, 'svnadmin').run0('create', path)
1093 commandline(ui, 'svnadmin').run0('create', path)
1094 created = path
1094 created = path
1095 path = util.normpath(path)
1095 path = util.normpath(path)
1096 if not path.startswith('/'):
1096 if not path.startswith('/'):
1097 path = '/' + path
1097 path = '/' + path
1098 path = 'file://' + path
1098 path = 'file://' + path
1099
1099
1100 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
1100 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
1101 ui.status(_('initializing svn working copy %r\n')
1101 ui.status(_('initializing svn working copy %r\n')
1102 % os.path.basename(wcpath))
1102 % os.path.basename(wcpath))
1103 self.run0('checkout', path, wcpath)
1103 self.run0('checkout', path, wcpath)
1104
1104
1105 self.wc = wcpath
1105 self.wc = wcpath
1106 self.opener = scmutil.opener(self.wc)
1106 self.opener = scmutil.opener(self.wc)
1107 self.wopener = scmutil.opener(self.wc)
1107 self.wopener = scmutil.opener(self.wc)
1108 self.childmap = mapfile(ui, self.join('hg-childmap'))
1108 self.childmap = mapfile(ui, self.join('hg-childmap'))
1109 self.is_exec = util.checkexec(self.wc) and util.isexec or None
1109 self.is_exec = util.checkexec(self.wc) and util.isexec or None
1110
1110
1111 if created:
1111 if created:
1112 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1112 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1113 fp = open(hook, 'w')
1113 fp = open(hook, 'w')
1114 fp.write(pre_revprop_change)
1114 fp.write(pre_revprop_change)
1115 fp.close()
1115 fp.close()
1116 util.setflags(hook, False, True)
1116 util.setflags(hook, False, True)
1117
1117
1118 output = self.run0('info')
1118 output = self.run0('info')
1119 self.uuid = self.uuid_re.search(output).group(1).strip()
1119 self.uuid = self.uuid_re.search(output).group(1).strip()
1120
1120
1121 def wjoin(self, *names):
1121 def wjoin(self, *names):
1122 return os.path.join(self.wc, *names)
1122 return os.path.join(self.wc, *names)
1123
1123
1124 @propertycache
1124 @propertycache
1125 def manifest(self):
1125 def manifest(self):
1126 # As of svn 1.7, the "add" command fails when receiving
1126 # As of svn 1.7, the "add" command fails when receiving
1127 # already tracked entries, so we have to track and filter them
1127 # already tracked entries, so we have to track and filter them
1128 # ourselves.
1128 # ourselves.
1129 m = set()
1129 m = set()
1130 output = self.run0('ls', recursive=True, xml=True)
1130 output = self.run0('ls', recursive=True, xml=True)
1131 doc = xml.dom.minidom.parseString(output)
1131 doc = xml.dom.minidom.parseString(output)
1132 for e in doc.getElementsByTagName('entry'):
1132 for e in doc.getElementsByTagName('entry'):
1133 for n in e.childNodes:
1133 for n in e.childNodes:
1134 if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
1134 if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
1135 continue
1135 continue
1136 name = ''.join(c.data for c in n.childNodes
1136 name = ''.join(c.data for c in n.childNodes
1137 if c.nodeType == c.TEXT_NODE)
1137 if c.nodeType == c.TEXT_NODE)
1138 # Entries are compared with names coming from
1138 # Entries are compared with names coming from
1139 # mercurial, so bytes with undefined encoding. Our
1139 # mercurial, so bytes with undefined encoding. Our
1140 # best bet is to assume they are in local
1140 # best bet is to assume they are in local
1141 # encoding. They will be passed to command line calls
1141 # encoding. They will be passed to command line calls
1142 # later anyway, so they better be.
1142 # later anyway, so they better be.
1143 m.add(encoding.tolocal(name.encode('utf-8')))
1143 m.add(encoding.tolocal(name.encode('utf-8')))
1144 break
1144 break
1145 return m
1145 return m
1146
1146
1147 def putfile(self, filename, flags, data):
1147 def putfile(self, filename, flags, data):
1148 if 'l' in flags:
1148 if 'l' in flags:
1149 self.wopener.symlink(data, filename)
1149 self.wopener.symlink(data, filename)
1150 else:
1150 else:
1151 try:
1151 try:
1152 if os.path.islink(self.wjoin(filename)):
1152 if os.path.islink(self.wjoin(filename)):
1153 os.unlink(filename)
1153 os.unlink(filename)
1154 except OSError:
1154 except OSError:
1155 pass
1155 pass
1156 self.wopener.write(filename, data)
1156 self.wopener.write(filename, data)
1157
1157
1158 if self.is_exec:
1158 if self.is_exec:
1159 if self.is_exec(self.wjoin(filename)):
1159 if self.is_exec(self.wjoin(filename)):
1160 if 'x' not in flags:
1160 if 'x' not in flags:
1161 self.delexec.append(filename)
1161 self.delexec.append(filename)
1162 else:
1162 else:
1163 if 'x' in flags:
1163 if 'x' in flags:
1164 self.setexec.append(filename)
1164 self.setexec.append(filename)
1165 util.setflags(self.wjoin(filename), False, 'x' in flags)
1165 util.setflags(self.wjoin(filename), False, 'x' in flags)
1166
1166
1167 def _copyfile(self, source, dest):
1167 def _copyfile(self, source, dest):
1168 # SVN's copy command pukes if the destination file exists, but
1168 # SVN's copy command pukes if the destination file exists, but
1169 # our copyfile method expects to record a copy that has
1169 # our copyfile method expects to record a copy that has
1170 # already occurred. Cross the semantic gap.
1170 # already occurred. Cross the semantic gap.
1171 wdest = self.wjoin(dest)
1171 wdest = self.wjoin(dest)
1172 exists = os.path.lexists(wdest)
1172 exists = os.path.lexists(wdest)
1173 if exists:
1173 if exists:
1174 fd, tempname = tempfile.mkstemp(
1174 fd, tempname = tempfile.mkstemp(
1175 prefix='hg-copy-', dir=os.path.dirname(wdest))
1175 prefix='hg-copy-', dir=os.path.dirname(wdest))
1176 os.close(fd)
1176 os.close(fd)
1177 os.unlink(tempname)
1177 os.unlink(tempname)
1178 os.rename(wdest, tempname)
1178 os.rename(wdest, tempname)
1179 try:
1179 try:
1180 self.run0('copy', source, dest)
1180 self.run0('copy', source, dest)
1181 finally:
1181 finally:
1182 self.manifest.add(dest)
1182 self.manifest.add(dest)
1183 if exists:
1183 if exists:
1184 try:
1184 try:
1185 os.unlink(wdest)
1185 os.unlink(wdest)
1186 except OSError:
1186 except OSError:
1187 pass
1187 pass
1188 os.rename(tempname, wdest)
1188 os.rename(tempname, wdest)
1189
1189
1190 def dirs_of(self, files):
1190 def dirs_of(self, files):
1191 dirs = set()
1191 dirs = set()
1192 for f in files:
1192 for f in files:
1193 if os.path.isdir(self.wjoin(f)):
1193 if os.path.isdir(self.wjoin(f)):
1194 dirs.add(f)
1194 dirs.add(f)
1195 for i in strutil.rfindall(f, '/'):
1195 for i in strutil.rfindall(f, '/'):
1196 dirs.add(f[:i])
1196 dirs.add(f[:i])
1197 return dirs
1197 return dirs
1198
1198
1199 def add_dirs(self, files):
1199 def add_dirs(self, files):
1200 add_dirs = [d for d in sorted(self.dirs_of(files))
1200 add_dirs = [d for d in sorted(self.dirs_of(files))
1201 if d not in self.manifest]
1201 if d not in self.manifest]
1202 if add_dirs:
1202 if add_dirs:
1203 self.manifest.update(add_dirs)
1203 self.manifest.update(add_dirs)
1204 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1204 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1205 return add_dirs
1205 return add_dirs
1206
1206
1207 def add_files(self, files):
1207 def add_files(self, files):
1208 files = [f for f in files if f not in self.manifest]
1208 files = [f for f in files if f not in self.manifest]
1209 if files:
1209 if files:
1210 self.manifest.update(files)
1210 self.manifest.update(files)
1211 self.xargs(files, 'add', quiet=True)
1211 self.xargs(files, 'add', quiet=True)
1212 return files
1212 return files
1213
1213
1214 def tidy_dirs(self, names):
1214 def tidy_dirs(self, names):
1215 deleted = []
1215 deleted = []
1216 for d in sorted(self.dirs_of(names), reverse=True):
1216 for d in sorted(self.dirs_of(names), reverse=True):
1217 wd = self.wjoin(d)
1217 wd = self.wjoin(d)
1218 if os.listdir(wd) == '.svn':
1218 if os.listdir(wd) == '.svn':
1219 self.run0('delete', d)
1219 self.run0('delete', d)
1220 self.manifest.remove(d)
1220 self.manifest.remove(d)
1221 deleted.append(d)
1221 deleted.append(d)
1222 return deleted
1222 return deleted
1223
1223
1224 def addchild(self, parent, child):
1224 def addchild(self, parent, child):
1225 self.childmap[parent] = child
1225 self.childmap[parent] = child
1226
1226
1227 def revid(self, rev):
1227 def revid(self, rev):
1228 return u"svn:%s@%s" % (self.uuid, rev)
1228 return u"svn:%s@%s" % (self.uuid, rev)
1229
1229
1230 def putcommit(self, files, copies, parents, commit, source, revmap):
1230 def putcommit(self, files, copies, parents, commit, source, revmap):
1231 for parent in parents:
1231 for parent in parents:
1232 try:
1232 try:
1233 return self.revid(self.childmap[parent])
1233 return self.revid(self.childmap[parent])
1234 except KeyError:
1234 except KeyError:
1235 pass
1235 pass
1236
1236
1237 # Apply changes to working copy
1237 # Apply changes to working copy
1238 for f, v in files:
1238 for f, v in files:
1239 try:
1239 try:
1240 data, mode = source.getfile(f, v)
1240 data, mode = source.getfile(f, v)
1241 except IOError:
1241 except IOError:
1242 self.delete.append(f)
1242 self.delete.append(f)
1243 else:
1243 else:
1244 self.putfile(f, mode, data)
1244 self.putfile(f, mode, data)
1245 if f in copies:
1245 if f in copies:
1246 self.copies.append([copies[f], f])
1246 self.copies.append([copies[f], f])
1247 files = [f[0] for f in files]
1247 files = [f[0] for f in files]
1248
1248
1249 entries = set(self.delete)
1249 entries = set(self.delete)
1250 files = frozenset(files)
1250 files = frozenset(files)
1251 entries.update(self.add_dirs(files.difference(entries)))
1251 entries.update(self.add_dirs(files.difference(entries)))
1252 if self.copies:
1252 if self.copies:
1253 for s, d in self.copies:
1253 for s, d in self.copies:
1254 self._copyfile(s, d)
1254 self._copyfile(s, d)
1255 self.copies = []
1255 self.copies = []
1256 if self.delete:
1256 if self.delete:
1257 self.xargs(self.delete, 'delete')
1257 self.xargs(self.delete, 'delete')
1258 for f in self.delete:
1258 for f in self.delete:
1259 self.manifest.remove(f)
1259 self.manifest.remove(f)
1260 self.delete = []
1260 self.delete = []
1261 entries.update(self.add_files(files.difference(entries)))
1261 entries.update(self.add_files(files.difference(entries)))
1262 entries.update(self.tidy_dirs(entries))
1262 entries.update(self.tidy_dirs(entries))
1263 if self.delexec:
1263 if self.delexec:
1264 self.xargs(self.delexec, 'propdel', 'svn:executable')
1264 self.xargs(self.delexec, 'propdel', 'svn:executable')
1265 self.delexec = []
1265 self.delexec = []
1266 if self.setexec:
1266 if self.setexec:
1267 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1267 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1268 self.setexec = []
1268 self.setexec = []
1269
1269
1270 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1270 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1271 fp = os.fdopen(fd, 'w')
1271 fp = os.fdopen(fd, 'w')
1272 fp.write(commit.desc)
1272 fp.write(commit.desc)
1273 fp.close()
1273 fp.close()
1274 try:
1274 try:
1275 output = self.run0('commit',
1275 output = self.run0('commit',
1276 username=util.shortuser(commit.author),
1276 username=util.shortuser(commit.author),
1277 file=messagefile,
1277 file=messagefile,
1278 encoding='utf-8')
1278 encoding='utf-8')
1279 try:
1279 try:
1280 rev = self.commit_re.search(output).group(1)
1280 rev = self.commit_re.search(output).group(1)
1281 except AttributeError:
1281 except AttributeError:
1282 if not files:
1282 if not files:
1283 return parents[0]
1283 return parents[0]
1284 self.ui.warn(_('unexpected svn output:\n'))
1284 self.ui.warn(_('unexpected svn output:\n'))
1285 self.ui.warn(output)
1285 self.ui.warn(output)
1286 raise util.Abort(_('unable to cope with svn output'))
1286 raise util.Abort(_('unable to cope with svn output'))
1287 if commit.rev:
1287 if commit.rev:
1288 self.run('propset', 'hg:convert-rev', commit.rev,
1288 self.run('propset', 'hg:convert-rev', commit.rev,
1289 revprop=True, revision=rev)
1289 revprop=True, revision=rev)
1290 if commit.branch and commit.branch != 'default':
1290 if commit.branch and commit.branch != 'default':
1291 self.run('propset', 'hg:convert-branch', commit.branch,
1291 self.run('propset', 'hg:convert-branch', commit.branch,
1292 revprop=True, revision=rev)
1292 revprop=True, revision=rev)
1293 for parent in parents:
1293 for parent in parents:
1294 self.addchild(parent, rev)
1294 self.addchild(parent, rev)
1295 return self.revid(rev)
1295 return self.revid(rev)
1296 finally:
1296 finally:
1297 os.unlink(messagefile)
1297 os.unlink(messagefile)
1298
1298
1299 def puttags(self, tags):
1299 def puttags(self, tags):
1300 self.ui.warn(_('writing Subversion tags is not yet implemented\n'))
1300 self.ui.warn(_('writing Subversion tags is not yet implemented\n'))
1301 return None, None
1301 return None, None
1302
1302
1303 def hascommit(self, rev):
1303 def hascommitforsplicemap(self, rev):
1304 # This is not correct as one can convert to an existing subversion
1304 # This is not correct as one can convert to an existing subversion
1305 # repository and childmap would not list all revisions. Too bad.
1305 # repository and childmap would not list all revisions. Too bad.
1306 if rev in self.childmap:
1306 if rev in self.childmap:
1307 return True
1307 return True
1308 raise util.Abort(_('splice map revision %s not found in subversion '
1308 raise util.Abort(_('splice map revision %s not found in subversion '
1309 'child map (revision lookups are not implemented)')
1309 'child map (revision lookups are not implemented)')
1310 % rev)
1310 % rev)
General Comments 0
You need to be logged in to leave comments. Login now