##// END OF EJS Templates
convert: specify unit for ui.progress when scanning paths
av6 -
r28471:f106913c default
parent child Browse files
Show More
@@ -1,1355 +1,1355
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 from __future__ import absolute_import
4 from __future__ import absolute_import
5
5
6 import cPickle as pickle
6 import cPickle as pickle
7 import cStringIO
7 import cStringIO
8 import os
8 import os
9 import re
9 import re
10 import sys
10 import sys
11 import tempfile
11 import tempfile
12 import urllib
12 import urllib
13 import urllib2
13 import urllib2
14 import xml.dom.minidom
14 import xml.dom.minidom
15
15
16 from mercurial import (
16 from mercurial import (
17 encoding,
17 encoding,
18 error,
18 error,
19 scmutil,
19 scmutil,
20 strutil,
20 strutil,
21 util,
21 util,
22 )
22 )
23 from mercurial.i18n import _
23 from mercurial.i18n import _
24
24
25 from . import common
25 from . import common
26
26
27 StringIO = cStringIO.StringIO
27 StringIO = cStringIO.StringIO
28 propertycache = util.propertycache
28 propertycache = util.propertycache
29
29
30 commandline = common.commandline
30 commandline = common.commandline
31 commit = common.commit
31 commit = common.commit
32 converter_sink = common.converter_sink
32 converter_sink = common.converter_sink
33 converter_source = common.converter_source
33 converter_source = common.converter_source
34 decodeargs = common.decodeargs
34 decodeargs = common.decodeargs
35 encodeargs = common.encodeargs
35 encodeargs = common.encodeargs
36 makedatetimestamp = common.makedatetimestamp
36 makedatetimestamp = common.makedatetimestamp
37 mapfile = common.mapfile
37 mapfile = common.mapfile
38 MissingTool = common.MissingTool
38 MissingTool = common.MissingTool
39 NoRepo = common.NoRepo
39 NoRepo = common.NoRepo
40
40
41 # Subversion stuff. Works best with very recent Python SVN bindings
41 # Subversion stuff. Works best with very recent Python SVN bindings
42 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
42 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
43 # these bindings.
43 # these bindings.
44
44
45 try:
45 try:
46 import svn
46 import svn
47 import svn.client
47 import svn.client
48 import svn.core
48 import svn.core
49 import svn.ra
49 import svn.ra
50 import svn.delta
50 import svn.delta
51 from . import transport
51 from . import transport
52 import warnings
52 import warnings
53 warnings.filterwarnings('ignore',
53 warnings.filterwarnings('ignore',
54 module='svn.core',
54 module='svn.core',
55 category=DeprecationWarning)
55 category=DeprecationWarning)
56 svn.core.SubversionException # trigger import to catch error
56 svn.core.SubversionException # trigger import to catch error
57
57
58 except ImportError:
58 except ImportError:
59 svn = None
59 svn = None
60
60
61 class SvnPathNotFound(Exception):
61 class SvnPathNotFound(Exception):
62 pass
62 pass
63
63
64 def revsplit(rev):
64 def revsplit(rev):
65 """Parse a revision string and return (uuid, path, revnum).
65 """Parse a revision string and return (uuid, path, revnum).
66 >>> revsplit('svn:a2147622-4a9f-4db4-a8d3-13562ff547b2'
66 >>> revsplit('svn:a2147622-4a9f-4db4-a8d3-13562ff547b2'
67 ... '/proj%20B/mytrunk/mytrunk@1')
67 ... '/proj%20B/mytrunk/mytrunk@1')
68 ('a2147622-4a9f-4db4-a8d3-13562ff547b2', '/proj%20B/mytrunk/mytrunk', 1)
68 ('a2147622-4a9f-4db4-a8d3-13562ff547b2', '/proj%20B/mytrunk/mytrunk', 1)
69 >>> revsplit('svn:8af66a51-67f5-4354-b62c-98d67cc7be1d@1')
69 >>> revsplit('svn:8af66a51-67f5-4354-b62c-98d67cc7be1d@1')
70 ('', '', 1)
70 ('', '', 1)
71 >>> revsplit('@7')
71 >>> revsplit('@7')
72 ('', '', 7)
72 ('', '', 7)
73 >>> revsplit('7')
73 >>> revsplit('7')
74 ('', '', 0)
74 ('', '', 0)
75 >>> revsplit('bad')
75 >>> revsplit('bad')
76 ('', '', 0)
76 ('', '', 0)
77 """
77 """
78 parts = rev.rsplit('@', 1)
78 parts = rev.rsplit('@', 1)
79 revnum = 0
79 revnum = 0
80 if len(parts) > 1:
80 if len(parts) > 1:
81 revnum = int(parts[1])
81 revnum = int(parts[1])
82 parts = parts[0].split('/', 1)
82 parts = parts[0].split('/', 1)
83 uuid = ''
83 uuid = ''
84 mod = ''
84 mod = ''
85 if len(parts) > 1 and parts[0].startswith('svn:'):
85 if len(parts) > 1 and parts[0].startswith('svn:'):
86 uuid = parts[0][4:]
86 uuid = parts[0][4:]
87 mod = '/' + parts[1]
87 mod = '/' + parts[1]
88 return uuid, mod, revnum
88 return uuid, mod, revnum
89
89
90 def quote(s):
90 def quote(s):
91 # As of svn 1.7, many svn calls expect "canonical" paths. In
91 # As of svn 1.7, many svn calls expect "canonical" paths. In
92 # theory, we should call svn.core.*canonicalize() on all paths
92 # theory, we should call svn.core.*canonicalize() on all paths
93 # before passing them to the API. Instead, we assume the base url
93 # before passing them to the API. Instead, we assume the base url
94 # is canonical and copy the behaviour of svn URL encoding function
94 # is canonical and copy the behaviour of svn URL encoding function
95 # so we can extend it safely with new components. The "safe"
95 # so we can extend it safely with new components. The "safe"
96 # characters were taken from the "svn_uri__char_validity" table in
96 # characters were taken from the "svn_uri__char_validity" table in
97 # libsvn_subr/path.c.
97 # libsvn_subr/path.c.
98 return urllib.quote(s, "!$&'()*+,-./:=@_~")
98 return urllib.quote(s, "!$&'()*+,-./:=@_~")
99
99
100 def geturl(path):
100 def geturl(path):
101 try:
101 try:
102 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
102 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
103 except svn.core.SubversionException:
103 except svn.core.SubversionException:
104 # svn.client.url_from_path() fails with local repositories
104 # svn.client.url_from_path() fails with local repositories
105 pass
105 pass
106 if os.path.isdir(path):
106 if os.path.isdir(path):
107 path = os.path.normpath(os.path.abspath(path))
107 path = os.path.normpath(os.path.abspath(path))
108 if os.name == 'nt':
108 if os.name == 'nt':
109 path = '/' + util.normpath(path)
109 path = '/' + util.normpath(path)
110 # Module URL is later compared with the repository URL returned
110 # Module URL is later compared with the repository URL returned
111 # by svn API, which is UTF-8.
111 # by svn API, which is UTF-8.
112 path = encoding.tolocal(path)
112 path = encoding.tolocal(path)
113 path = 'file://%s' % quote(path)
113 path = 'file://%s' % quote(path)
114 return svn.core.svn_path_canonicalize(path)
114 return svn.core.svn_path_canonicalize(path)
115
115
116 def optrev(number):
116 def optrev(number):
117 optrev = svn.core.svn_opt_revision_t()
117 optrev = svn.core.svn_opt_revision_t()
118 optrev.kind = svn.core.svn_opt_revision_number
118 optrev.kind = svn.core.svn_opt_revision_number
119 optrev.value.number = number
119 optrev.value.number = number
120 return optrev
120 return optrev
121
121
122 class changedpath(object):
122 class changedpath(object):
123 def __init__(self, p):
123 def __init__(self, p):
124 self.copyfrom_path = p.copyfrom_path
124 self.copyfrom_path = p.copyfrom_path
125 self.copyfrom_rev = p.copyfrom_rev
125 self.copyfrom_rev = p.copyfrom_rev
126 self.action = p.action
126 self.action = p.action
127
127
128 def get_log_child(fp, url, paths, start, end, limit=0,
128 def get_log_child(fp, url, paths, start, end, limit=0,
129 discover_changed_paths=True, strict_node_history=False):
129 discover_changed_paths=True, strict_node_history=False):
130 protocol = -1
130 protocol = -1
131 def receiver(orig_paths, revnum, author, date, message, pool):
131 def receiver(orig_paths, revnum, author, date, message, pool):
132 paths = {}
132 paths = {}
133 if orig_paths is not None:
133 if orig_paths is not None:
134 for k, v in orig_paths.iteritems():
134 for k, v in orig_paths.iteritems():
135 paths[k] = changedpath(v)
135 paths[k] = changedpath(v)
136 pickle.dump((paths, revnum, author, date, message),
136 pickle.dump((paths, revnum, author, date, message),
137 fp, protocol)
137 fp, protocol)
138
138
139 try:
139 try:
140 # Use an ra of our own so that our parent can consume
140 # Use an ra of our own so that our parent can consume
141 # our results without confusing the server.
141 # our results without confusing the server.
142 t = transport.SvnRaTransport(url=url)
142 t = transport.SvnRaTransport(url=url)
143 svn.ra.get_log(t.ra, paths, start, end, limit,
143 svn.ra.get_log(t.ra, paths, start, end, limit,
144 discover_changed_paths,
144 discover_changed_paths,
145 strict_node_history,
145 strict_node_history,
146 receiver)
146 receiver)
147 except IOError:
147 except IOError:
148 # Caller may interrupt the iteration
148 # Caller may interrupt the iteration
149 pickle.dump(None, fp, protocol)
149 pickle.dump(None, fp, protocol)
150 except Exception as inst:
150 except Exception as inst:
151 pickle.dump(str(inst), fp, protocol)
151 pickle.dump(str(inst), fp, protocol)
152 else:
152 else:
153 pickle.dump(None, fp, protocol)
153 pickle.dump(None, fp, protocol)
154 fp.close()
154 fp.close()
155 # With large history, cleanup process goes crazy and suddenly
155 # With large history, cleanup process goes crazy and suddenly
156 # consumes *huge* amount of memory. The output file being closed,
156 # consumes *huge* amount of memory. The output file being closed,
157 # there is no need for clean termination.
157 # there is no need for clean termination.
158 os._exit(0)
158 os._exit(0)
159
159
160 def debugsvnlog(ui, **opts):
160 def debugsvnlog(ui, **opts):
161 """Fetch SVN log in a subprocess and channel them back to parent to
161 """Fetch SVN log in a subprocess and channel them back to parent to
162 avoid memory collection issues.
162 avoid memory collection issues.
163 """
163 """
164 if svn is None:
164 if svn is None:
165 raise error.Abort(_('debugsvnlog could not load Subversion python '
165 raise error.Abort(_('debugsvnlog could not load Subversion python '
166 'bindings'))
166 'bindings'))
167
167
168 util.setbinary(sys.stdin)
168 util.setbinary(sys.stdin)
169 util.setbinary(sys.stdout)
169 util.setbinary(sys.stdout)
170 args = decodeargs(sys.stdin.read())
170 args = decodeargs(sys.stdin.read())
171 get_log_child(sys.stdout, *args)
171 get_log_child(sys.stdout, *args)
172
172
173 class logstream(object):
173 class logstream(object):
174 """Interruptible revision log iterator."""
174 """Interruptible revision log iterator."""
175 def __init__(self, stdout):
175 def __init__(self, stdout):
176 self._stdout = stdout
176 self._stdout = stdout
177
177
178 def __iter__(self):
178 def __iter__(self):
179 while True:
179 while True:
180 try:
180 try:
181 entry = pickle.load(self._stdout)
181 entry = pickle.load(self._stdout)
182 except EOFError:
182 except EOFError:
183 raise error.Abort(_('Mercurial failed to run itself, check'
183 raise error.Abort(_('Mercurial failed to run itself, check'
184 ' hg executable is in PATH'))
184 ' hg executable is in PATH'))
185 try:
185 try:
186 orig_paths, revnum, author, date, message = entry
186 orig_paths, revnum, author, date, message = entry
187 except (TypeError, ValueError):
187 except (TypeError, ValueError):
188 if entry is None:
188 if entry is None:
189 break
189 break
190 raise error.Abort(_("log stream exception '%s'") % entry)
190 raise error.Abort(_("log stream exception '%s'") % entry)
191 yield entry
191 yield entry
192
192
193 def close(self):
193 def close(self):
194 if self._stdout:
194 if self._stdout:
195 self._stdout.close()
195 self._stdout.close()
196 self._stdout = None
196 self._stdout = None
197
197
198 class directlogstream(list):
198 class directlogstream(list):
199 """Direct revision log iterator.
199 """Direct revision log iterator.
200 This can be used for debugging and development but it will probably leak
200 This can be used for debugging and development but it will probably leak
201 memory and is not suitable for real conversions."""
201 memory and is not suitable for real conversions."""
202 def __init__(self, url, paths, start, end, limit=0,
202 def __init__(self, url, paths, start, end, limit=0,
203 discover_changed_paths=True, strict_node_history=False):
203 discover_changed_paths=True, strict_node_history=False):
204
204
205 def receiver(orig_paths, revnum, author, date, message, pool):
205 def receiver(orig_paths, revnum, author, date, message, pool):
206 paths = {}
206 paths = {}
207 if orig_paths is not None:
207 if orig_paths is not None:
208 for k, v in orig_paths.iteritems():
208 for k, v in orig_paths.iteritems():
209 paths[k] = changedpath(v)
209 paths[k] = changedpath(v)
210 self.append((paths, revnum, author, date, message))
210 self.append((paths, revnum, author, date, message))
211
211
212 # Use an ra of our own so that our parent can consume
212 # Use an ra of our own so that our parent can consume
213 # our results without confusing the server.
213 # our results without confusing the server.
214 t = transport.SvnRaTransport(url=url)
214 t = transport.SvnRaTransport(url=url)
215 svn.ra.get_log(t.ra, paths, start, end, limit,
215 svn.ra.get_log(t.ra, paths, start, end, limit,
216 discover_changed_paths,
216 discover_changed_paths,
217 strict_node_history,
217 strict_node_history,
218 receiver)
218 receiver)
219
219
220 def close(self):
220 def close(self):
221 pass
221 pass
222
222
223 # Check to see if the given path is a local Subversion repo. Verify this by
223 # Check to see if the given path is a local Subversion repo. Verify this by
224 # looking for several svn-specific files and directories in the given
224 # looking for several svn-specific files and directories in the given
225 # directory.
225 # directory.
226 def filecheck(ui, path, proto):
226 def filecheck(ui, path, proto):
227 for x in ('locks', 'hooks', 'format', 'db'):
227 for x in ('locks', 'hooks', 'format', 'db'):
228 if not os.path.exists(os.path.join(path, x)):
228 if not os.path.exists(os.path.join(path, x)):
229 return False
229 return False
230 return True
230 return True
231
231
232 # Check to see if a given path is the root of an svn repo over http. We verify
232 # Check to see if a given path is the root of an svn repo over http. We verify
233 # this by requesting a version-controlled URL we know can't exist and looking
233 # this by requesting a version-controlled URL we know can't exist and looking
234 # for the svn-specific "not found" XML.
234 # for the svn-specific "not found" XML.
235 def httpcheck(ui, path, proto):
235 def httpcheck(ui, path, proto):
236 try:
236 try:
237 opener = urllib2.build_opener()
237 opener = urllib2.build_opener()
238 rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path))
238 rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path))
239 data = rsp.read()
239 data = rsp.read()
240 except urllib2.HTTPError as inst:
240 except urllib2.HTTPError as inst:
241 if inst.code != 404:
241 if inst.code != 404:
242 # Except for 404 we cannot know for sure this is not an svn repo
242 # Except for 404 we cannot know for sure this is not an svn repo
243 ui.warn(_('svn: cannot probe remote repository, assume it could '
243 ui.warn(_('svn: cannot probe remote repository, assume it could '
244 'be a subversion repository. Use --source-type if you '
244 'be a subversion repository. Use --source-type if you '
245 'know better.\n'))
245 'know better.\n'))
246 return True
246 return True
247 data = inst.fp.read()
247 data = inst.fp.read()
248 except Exception:
248 except Exception:
249 # Could be urllib2.URLError if the URL is invalid or anything else.
249 # Could be urllib2.URLError if the URL is invalid or anything else.
250 return False
250 return False
251 return '<m:human-readable errcode="160013">' in data
251 return '<m:human-readable errcode="160013">' in data
252
252
253 protomap = {'http': httpcheck,
253 protomap = {'http': httpcheck,
254 'https': httpcheck,
254 'https': httpcheck,
255 'file': filecheck,
255 'file': filecheck,
256 }
256 }
257 def issvnurl(ui, url):
257 def issvnurl(ui, url):
258 try:
258 try:
259 proto, path = url.split('://', 1)
259 proto, path = url.split('://', 1)
260 if proto == 'file':
260 if proto == 'file':
261 if (os.name == 'nt' and path[:1] == '/' and path[1:2].isalpha()
261 if (os.name == 'nt' and path[:1] == '/' and path[1:2].isalpha()
262 and path[2:6].lower() == '%3a/'):
262 and path[2:6].lower() == '%3a/'):
263 path = path[:2] + ':/' + path[6:]
263 path = path[:2] + ':/' + path[6:]
264 path = urllib.url2pathname(path)
264 path = urllib.url2pathname(path)
265 except ValueError:
265 except ValueError:
266 proto = 'file'
266 proto = 'file'
267 path = os.path.abspath(url)
267 path = os.path.abspath(url)
268 if proto == 'file':
268 if proto == 'file':
269 path = util.pconvert(path)
269 path = util.pconvert(path)
270 check = protomap.get(proto, lambda *args: False)
270 check = protomap.get(proto, lambda *args: False)
271 while '/' in path:
271 while '/' in path:
272 if check(ui, path, proto):
272 if check(ui, path, proto):
273 return True
273 return True
274 path = path.rsplit('/', 1)[0]
274 path = path.rsplit('/', 1)[0]
275 return False
275 return False
276
276
277 # SVN conversion code stolen from bzr-svn and tailor
277 # SVN conversion code stolen from bzr-svn and tailor
278 #
278 #
279 # Subversion looks like a versioned filesystem, branches structures
279 # Subversion looks like a versioned filesystem, branches structures
280 # are defined by conventions and not enforced by the tool. First,
280 # are defined by conventions and not enforced by the tool. First,
281 # we define the potential branches (modules) as "trunk" and "branches"
281 # we define the potential branches (modules) as "trunk" and "branches"
282 # children directories. Revisions are then identified by their
282 # children directories. Revisions are then identified by their
283 # module and revision number (and a repository identifier).
283 # module and revision number (and a repository identifier).
284 #
284 #
285 # The revision graph is really a tree (or a forest). By default, a
285 # The revision graph is really a tree (or a forest). By default, a
286 # revision parent is the previous revision in the same module. If the
286 # revision parent is the previous revision in the same module. If the
287 # module directory is copied/moved from another module then the
287 # module directory is copied/moved from another module then the
288 # revision is the module root and its parent the source revision in
288 # revision is the module root and its parent the source revision in
289 # the parent module. A revision has at most one parent.
289 # the parent module. A revision has at most one parent.
290 #
290 #
291 class svn_source(converter_source):
291 class svn_source(converter_source):
292 def __init__(self, ui, url, revs=None):
292 def __init__(self, ui, url, revs=None):
293 super(svn_source, self).__init__(ui, url, revs=revs)
293 super(svn_source, self).__init__(ui, url, revs=revs)
294
294
295 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
295 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
296 (os.path.exists(url) and
296 (os.path.exists(url) and
297 os.path.exists(os.path.join(url, '.svn'))) or
297 os.path.exists(os.path.join(url, '.svn'))) or
298 issvnurl(ui, url)):
298 issvnurl(ui, url)):
299 raise NoRepo(_("%s does not look like a Subversion repository")
299 raise NoRepo(_("%s does not look like a Subversion repository")
300 % url)
300 % url)
301 if svn is None:
301 if svn is None:
302 raise MissingTool(_('could not load Subversion python bindings'))
302 raise MissingTool(_('could not load Subversion python bindings'))
303
303
304 try:
304 try:
305 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
305 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
306 if version < (1, 4):
306 if version < (1, 4):
307 raise MissingTool(_('Subversion python bindings %d.%d found, '
307 raise MissingTool(_('Subversion python bindings %d.%d found, '
308 '1.4 or later required') % version)
308 '1.4 or later required') % version)
309 except AttributeError:
309 except AttributeError:
310 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
310 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
311 'or later required'))
311 'or later required'))
312
312
313 self.lastrevs = {}
313 self.lastrevs = {}
314
314
315 latest = None
315 latest = None
316 try:
316 try:
317 # Support file://path@rev syntax. Useful e.g. to convert
317 # Support file://path@rev syntax. Useful e.g. to convert
318 # deleted branches.
318 # deleted branches.
319 at = url.rfind('@')
319 at = url.rfind('@')
320 if at >= 0:
320 if at >= 0:
321 latest = int(url[at + 1:])
321 latest = int(url[at + 1:])
322 url = url[:at]
322 url = url[:at]
323 except ValueError:
323 except ValueError:
324 pass
324 pass
325 self.url = geturl(url)
325 self.url = geturl(url)
326 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
326 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
327 try:
327 try:
328 self.transport = transport.SvnRaTransport(url=self.url)
328 self.transport = transport.SvnRaTransport(url=self.url)
329 self.ra = self.transport.ra
329 self.ra = self.transport.ra
330 self.ctx = self.transport.client
330 self.ctx = self.transport.client
331 self.baseurl = svn.ra.get_repos_root(self.ra)
331 self.baseurl = svn.ra.get_repos_root(self.ra)
332 # Module is either empty or a repository path starting with
332 # Module is either empty or a repository path starting with
333 # a slash and not ending with a slash.
333 # a slash and not ending with a slash.
334 self.module = urllib.unquote(self.url[len(self.baseurl):])
334 self.module = urllib.unquote(self.url[len(self.baseurl):])
335 self.prevmodule = None
335 self.prevmodule = None
336 self.rootmodule = self.module
336 self.rootmodule = self.module
337 self.commits = {}
337 self.commits = {}
338 self.paths = {}
338 self.paths = {}
339 self.uuid = svn.ra.get_uuid(self.ra)
339 self.uuid = svn.ra.get_uuid(self.ra)
340 except svn.core.SubversionException:
340 except svn.core.SubversionException:
341 ui.traceback()
341 ui.traceback()
342 svnversion = '%d.%d.%d' % (svn.core.SVN_VER_MAJOR,
342 svnversion = '%d.%d.%d' % (svn.core.SVN_VER_MAJOR,
343 svn.core.SVN_VER_MINOR,
343 svn.core.SVN_VER_MINOR,
344 svn.core.SVN_VER_MICRO)
344 svn.core.SVN_VER_MICRO)
345 raise NoRepo(_("%s does not look like a Subversion repository "
345 raise NoRepo(_("%s does not look like a Subversion repository "
346 "to libsvn version %s")
346 "to libsvn version %s")
347 % (self.url, svnversion))
347 % (self.url, svnversion))
348
348
349 if revs:
349 if revs:
350 if len(revs) > 1:
350 if len(revs) > 1:
351 raise error.Abort(_('subversion source does not support '
351 raise error.Abort(_('subversion source does not support '
352 'specifying multiple revisions'))
352 'specifying multiple revisions'))
353 try:
353 try:
354 latest = int(revs[0])
354 latest = int(revs[0])
355 except ValueError:
355 except ValueError:
356 raise error.Abort(_('svn: revision %s is not an integer') %
356 raise error.Abort(_('svn: revision %s is not an integer') %
357 revs[0])
357 revs[0])
358
358
359 self.trunkname = self.ui.config('convert', 'svn.trunk',
359 self.trunkname = self.ui.config('convert', 'svn.trunk',
360 'trunk').strip('/')
360 'trunk').strip('/')
361 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
361 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
362 try:
362 try:
363 self.startrev = int(self.startrev)
363 self.startrev = int(self.startrev)
364 if self.startrev < 0:
364 if self.startrev < 0:
365 self.startrev = 0
365 self.startrev = 0
366 except ValueError:
366 except ValueError:
367 raise error.Abort(_('svn: start revision %s is not an integer')
367 raise error.Abort(_('svn: start revision %s is not an integer')
368 % self.startrev)
368 % self.startrev)
369
369
370 try:
370 try:
371 self.head = self.latest(self.module, latest)
371 self.head = self.latest(self.module, latest)
372 except SvnPathNotFound:
372 except SvnPathNotFound:
373 self.head = None
373 self.head = None
374 if not self.head:
374 if not self.head:
375 raise error.Abort(_('no revision found in module %s')
375 raise error.Abort(_('no revision found in module %s')
376 % self.module)
376 % self.module)
377 self.last_changed = self.revnum(self.head)
377 self.last_changed = self.revnum(self.head)
378
378
379 self._changescache = (None, None)
379 self._changescache = (None, None)
380
380
381 if os.path.exists(os.path.join(url, '.svn/entries')):
381 if os.path.exists(os.path.join(url, '.svn/entries')):
382 self.wc = url
382 self.wc = url
383 else:
383 else:
384 self.wc = None
384 self.wc = None
385 self.convertfp = None
385 self.convertfp = None
386
386
387 def setrevmap(self, revmap):
387 def setrevmap(self, revmap):
388 lastrevs = {}
388 lastrevs = {}
389 for revid in revmap.iterkeys():
389 for revid in revmap.iterkeys():
390 uuid, module, revnum = revsplit(revid)
390 uuid, module, revnum = revsplit(revid)
391 lastrevnum = lastrevs.setdefault(module, revnum)
391 lastrevnum = lastrevs.setdefault(module, revnum)
392 if revnum > lastrevnum:
392 if revnum > lastrevnum:
393 lastrevs[module] = revnum
393 lastrevs[module] = revnum
394 self.lastrevs = lastrevs
394 self.lastrevs = lastrevs
395
395
396 def exists(self, path, optrev):
396 def exists(self, path, optrev):
397 try:
397 try:
398 svn.client.ls(self.url.rstrip('/') + '/' + quote(path),
398 svn.client.ls(self.url.rstrip('/') + '/' + quote(path),
399 optrev, False, self.ctx)
399 optrev, False, self.ctx)
400 return True
400 return True
401 except svn.core.SubversionException:
401 except svn.core.SubversionException:
402 return False
402 return False
403
403
404 def getheads(self):
404 def getheads(self):
405
405
406 def isdir(path, revnum):
406 def isdir(path, revnum):
407 kind = self._checkpath(path, revnum)
407 kind = self._checkpath(path, revnum)
408 return kind == svn.core.svn_node_dir
408 return kind == svn.core.svn_node_dir
409
409
410 def getcfgpath(name, rev):
410 def getcfgpath(name, rev):
411 cfgpath = self.ui.config('convert', 'svn.' + name)
411 cfgpath = self.ui.config('convert', 'svn.' + name)
412 if cfgpath is not None and cfgpath.strip() == '':
412 if cfgpath is not None and cfgpath.strip() == '':
413 return None
413 return None
414 path = (cfgpath or name).strip('/')
414 path = (cfgpath or name).strip('/')
415 if not self.exists(path, rev):
415 if not self.exists(path, rev):
416 if self.module.endswith(path) and name == 'trunk':
416 if self.module.endswith(path) and name == 'trunk':
417 # we are converting from inside this directory
417 # we are converting from inside this directory
418 return None
418 return None
419 if cfgpath:
419 if cfgpath:
420 raise error.Abort(_('expected %s to be at %r, but not found'
420 raise error.Abort(_('expected %s to be at %r, but not found'
421 ) % (name, path))
421 ) % (name, path))
422 return None
422 return None
423 self.ui.note(_('found %s at %r\n') % (name, path))
423 self.ui.note(_('found %s at %r\n') % (name, path))
424 return path
424 return path
425
425
426 rev = optrev(self.last_changed)
426 rev = optrev(self.last_changed)
427 oldmodule = ''
427 oldmodule = ''
428 trunk = getcfgpath('trunk', rev)
428 trunk = getcfgpath('trunk', rev)
429 self.tags = getcfgpath('tags', rev)
429 self.tags = getcfgpath('tags', rev)
430 branches = getcfgpath('branches', rev)
430 branches = getcfgpath('branches', rev)
431
431
432 # If the project has a trunk or branches, we will extract heads
432 # If the project has a trunk or branches, we will extract heads
433 # from them. We keep the project root otherwise.
433 # from them. We keep the project root otherwise.
434 if trunk:
434 if trunk:
435 oldmodule = self.module or ''
435 oldmodule = self.module or ''
436 self.module += '/' + trunk
436 self.module += '/' + trunk
437 self.head = self.latest(self.module, self.last_changed)
437 self.head = self.latest(self.module, self.last_changed)
438 if not self.head:
438 if not self.head:
439 raise error.Abort(_('no revision found in module %s')
439 raise error.Abort(_('no revision found in module %s')
440 % self.module)
440 % self.module)
441
441
442 # First head in the list is the module's head
442 # First head in the list is the module's head
443 self.heads = [self.head]
443 self.heads = [self.head]
444 if self.tags is not None:
444 if self.tags is not None:
445 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
445 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
446
446
447 # Check if branches bring a few more heads to the list
447 # Check if branches bring a few more heads to the list
448 if branches:
448 if branches:
449 rpath = self.url.strip('/')
449 rpath = self.url.strip('/')
450 branchnames = svn.client.ls(rpath + '/' + quote(branches),
450 branchnames = svn.client.ls(rpath + '/' + quote(branches),
451 rev, False, self.ctx)
451 rev, False, self.ctx)
452 for branch in sorted(branchnames):
452 for branch in sorted(branchnames):
453 module = '%s/%s/%s' % (oldmodule, branches, branch)
453 module = '%s/%s/%s' % (oldmodule, branches, branch)
454 if not isdir(module, self.last_changed):
454 if not isdir(module, self.last_changed):
455 continue
455 continue
456 brevid = self.latest(module, self.last_changed)
456 brevid = self.latest(module, self.last_changed)
457 if not brevid:
457 if not brevid:
458 self.ui.note(_('ignoring empty branch %s\n') % branch)
458 self.ui.note(_('ignoring empty branch %s\n') % branch)
459 continue
459 continue
460 self.ui.note(_('found branch %s at %d\n') %
460 self.ui.note(_('found branch %s at %d\n') %
461 (branch, self.revnum(brevid)))
461 (branch, self.revnum(brevid)))
462 self.heads.append(brevid)
462 self.heads.append(brevid)
463
463
464 if self.startrev and self.heads:
464 if self.startrev and self.heads:
465 if len(self.heads) > 1:
465 if len(self.heads) > 1:
466 raise error.Abort(_('svn: start revision is not supported '
466 raise error.Abort(_('svn: start revision is not supported '
467 'with more than one branch'))
467 'with more than one branch'))
468 revnum = self.revnum(self.heads[0])
468 revnum = self.revnum(self.heads[0])
469 if revnum < self.startrev:
469 if revnum < self.startrev:
470 raise error.Abort(
470 raise error.Abort(
471 _('svn: no revision found after start revision %d')
471 _('svn: no revision found after start revision %d')
472 % self.startrev)
472 % self.startrev)
473
473
474 return self.heads
474 return self.heads
475
475
476 def _getchanges(self, rev, full):
476 def _getchanges(self, rev, full):
477 (paths, parents) = self.paths[rev]
477 (paths, parents) = self.paths[rev]
478 copies = {}
478 copies = {}
479 if parents:
479 if parents:
480 files, self.removed, copies = self.expandpaths(rev, paths, parents)
480 files, self.removed, copies = self.expandpaths(rev, paths, parents)
481 if full or not parents:
481 if full or not parents:
482 # Perform a full checkout on roots
482 # Perform a full checkout on roots
483 uuid, module, revnum = revsplit(rev)
483 uuid, module, revnum = revsplit(rev)
484 entries = svn.client.ls(self.baseurl + quote(module),
484 entries = svn.client.ls(self.baseurl + quote(module),
485 optrev(revnum), True, self.ctx)
485 optrev(revnum), True, self.ctx)
486 files = [n for n, e in entries.iteritems()
486 files = [n for n, e in entries.iteritems()
487 if e.kind == svn.core.svn_node_file]
487 if e.kind == svn.core.svn_node_file]
488 self.removed = set()
488 self.removed = set()
489
489
490 files.sort()
490 files.sort()
491 files = zip(files, [rev] * len(files))
491 files = zip(files, [rev] * len(files))
492 return (files, copies)
492 return (files, copies)
493
493
494 def getchanges(self, rev, full):
494 def getchanges(self, rev, full):
495 # reuse cache from getchangedfiles
495 # reuse cache from getchangedfiles
496 if self._changescache[0] == rev and not full:
496 if self._changescache[0] == rev and not full:
497 (files, copies) = self._changescache[1]
497 (files, copies) = self._changescache[1]
498 else:
498 else:
499 (files, copies) = self._getchanges(rev, full)
499 (files, copies) = self._getchanges(rev, full)
500 # caller caches the result, so free it here to release memory
500 # caller caches the result, so free it here to release memory
501 del self.paths[rev]
501 del self.paths[rev]
502 return (files, copies, set())
502 return (files, copies, set())
503
503
504 def getchangedfiles(self, rev, i):
504 def getchangedfiles(self, rev, i):
505 # called from filemap - cache computed values for reuse in getchanges
505 # called from filemap - cache computed values for reuse in getchanges
506 (files, copies) = self._getchanges(rev, False)
506 (files, copies) = self._getchanges(rev, False)
507 self._changescache = (rev, (files, copies))
507 self._changescache = (rev, (files, copies))
508 return [f[0] for f in files]
508 return [f[0] for f in files]
509
509
510 def getcommit(self, rev):
510 def getcommit(self, rev):
511 if rev not in self.commits:
511 if rev not in self.commits:
512 uuid, module, revnum = revsplit(rev)
512 uuid, module, revnum = revsplit(rev)
513 self.module = module
513 self.module = module
514 self.reparent(module)
514 self.reparent(module)
515 # We assume that:
515 # We assume that:
516 # - requests for revisions after "stop" come from the
516 # - requests for revisions after "stop" come from the
517 # revision graph backward traversal. Cache all of them
517 # revision graph backward traversal. Cache all of them
518 # down to stop, they will be used eventually.
518 # down to stop, they will be used eventually.
519 # - requests for revisions before "stop" come to get
519 # - requests for revisions before "stop" come to get
520 # isolated branches parents. Just fetch what is needed.
520 # isolated branches parents. Just fetch what is needed.
521 stop = self.lastrevs.get(module, 0)
521 stop = self.lastrevs.get(module, 0)
522 if revnum < stop:
522 if revnum < stop:
523 stop = revnum + 1
523 stop = revnum + 1
524 self._fetch_revisions(revnum, stop)
524 self._fetch_revisions(revnum, stop)
525 if rev not in self.commits:
525 if rev not in self.commits:
526 raise error.Abort(_('svn: revision %s not found') % revnum)
526 raise error.Abort(_('svn: revision %s not found') % revnum)
527 revcommit = self.commits[rev]
527 revcommit = self.commits[rev]
528 # caller caches the result, so free it here to release memory
528 # caller caches the result, so free it here to release memory
529 del self.commits[rev]
529 del self.commits[rev]
530 return revcommit
530 return revcommit
531
531
532 def checkrevformat(self, revstr, mapname='splicemap'):
532 def checkrevformat(self, revstr, mapname='splicemap'):
533 """ fails if revision format does not match the correct format"""
533 """ fails if revision format does not match the correct format"""
534 if not re.match(r'svn:[0-9a-f]{8,8}-[0-9a-f]{4,4}-'
534 if not re.match(r'svn:[0-9a-f]{8,8}-[0-9a-f]{4,4}-'
535 '[0-9a-f]{4,4}-[0-9a-f]{4,4}-[0-9a-f]'
535 '[0-9a-f]{4,4}-[0-9a-f]{4,4}-[0-9a-f]'
536 '{12,12}(.*)\@[0-9]+$',revstr):
536 '{12,12}(.*)\@[0-9]+$',revstr):
537 raise error.Abort(_('%s entry %s is not a valid revision'
537 raise error.Abort(_('%s entry %s is not a valid revision'
538 ' identifier') % (mapname, revstr))
538 ' identifier') % (mapname, revstr))
539
539
540 def numcommits(self):
540 def numcommits(self):
541 return int(self.head.rsplit('@', 1)[1]) - self.startrev
541 return int(self.head.rsplit('@', 1)[1]) - self.startrev
542
542
543 def gettags(self):
543 def gettags(self):
544 tags = {}
544 tags = {}
545 if self.tags is None:
545 if self.tags is None:
546 return tags
546 return tags
547
547
548 # svn tags are just a convention, project branches left in a
548 # svn tags are just a convention, project branches left in a
549 # 'tags' directory. There is no other relationship than
549 # 'tags' directory. There is no other relationship than
550 # ancestry, which is expensive to discover and makes them hard
550 # ancestry, which is expensive to discover and makes them hard
551 # to update incrementally. Worse, past revisions may be
551 # to update incrementally. Worse, past revisions may be
552 # referenced by tags far away in the future, requiring a deep
552 # referenced by tags far away in the future, requiring a deep
553 # history traversal on every calculation. Current code
553 # history traversal on every calculation. Current code
554 # performs a single backward traversal, tracking moves within
554 # performs a single backward traversal, tracking moves within
555 # the tags directory (tag renaming) and recording a new tag
555 # the tags directory (tag renaming) and recording a new tag
556 # everytime a project is copied from outside the tags
556 # everytime a project is copied from outside the tags
557 # directory. It also lists deleted tags, this behaviour may
557 # directory. It also lists deleted tags, this behaviour may
558 # change in the future.
558 # change in the future.
559 pendings = []
559 pendings = []
560 tagspath = self.tags
560 tagspath = self.tags
561 start = svn.ra.get_latest_revnum(self.ra)
561 start = svn.ra.get_latest_revnum(self.ra)
562 stream = self._getlog([self.tags], start, self.startrev)
562 stream = self._getlog([self.tags], start, self.startrev)
563 try:
563 try:
564 for entry in stream:
564 for entry in stream:
565 origpaths, revnum, author, date, message = entry
565 origpaths, revnum, author, date, message = entry
566 if not origpaths:
566 if not origpaths:
567 origpaths = []
567 origpaths = []
568 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
568 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
569 in origpaths.iteritems() if e.copyfrom_path]
569 in origpaths.iteritems() if e.copyfrom_path]
570 # Apply moves/copies from more specific to general
570 # Apply moves/copies from more specific to general
571 copies.sort(reverse=True)
571 copies.sort(reverse=True)
572
572
573 srctagspath = tagspath
573 srctagspath = tagspath
574 if copies and copies[-1][2] == tagspath:
574 if copies and copies[-1][2] == tagspath:
575 # Track tags directory moves
575 # Track tags directory moves
576 srctagspath = copies.pop()[0]
576 srctagspath = copies.pop()[0]
577
577
578 for source, sourcerev, dest in copies:
578 for source, sourcerev, dest in copies:
579 if not dest.startswith(tagspath + '/'):
579 if not dest.startswith(tagspath + '/'):
580 continue
580 continue
581 for tag in pendings:
581 for tag in pendings:
582 if tag[0].startswith(dest):
582 if tag[0].startswith(dest):
583 tagpath = source + tag[0][len(dest):]
583 tagpath = source + tag[0][len(dest):]
584 tag[:2] = [tagpath, sourcerev]
584 tag[:2] = [tagpath, sourcerev]
585 break
585 break
586 else:
586 else:
587 pendings.append([source, sourcerev, dest])
587 pendings.append([source, sourcerev, dest])
588
588
589 # Filter out tags with children coming from different
589 # Filter out tags with children coming from different
590 # parts of the repository like:
590 # parts of the repository like:
591 # /tags/tag.1 (from /trunk:10)
591 # /tags/tag.1 (from /trunk:10)
592 # /tags/tag.1/foo (from /branches/foo:12)
592 # /tags/tag.1/foo (from /branches/foo:12)
593 # Here/tags/tag.1 discarded as well as its children.
593 # Here/tags/tag.1 discarded as well as its children.
594 # It happens with tools like cvs2svn. Such tags cannot
594 # It happens with tools like cvs2svn. Such tags cannot
595 # be represented in mercurial.
595 # be represented in mercurial.
596 addeds = dict((p, e.copyfrom_path) for p, e
596 addeds = dict((p, e.copyfrom_path) for p, e
597 in origpaths.iteritems()
597 in origpaths.iteritems()
598 if e.action == 'A' and e.copyfrom_path)
598 if e.action == 'A' and e.copyfrom_path)
599 badroots = set()
599 badroots = set()
600 for destroot in addeds:
600 for destroot in addeds:
601 for source, sourcerev, dest in pendings:
601 for source, sourcerev, dest in pendings:
602 if (not dest.startswith(destroot + '/')
602 if (not dest.startswith(destroot + '/')
603 or source.startswith(addeds[destroot] + '/')):
603 or source.startswith(addeds[destroot] + '/')):
604 continue
604 continue
605 badroots.add(destroot)
605 badroots.add(destroot)
606 break
606 break
607
607
608 for badroot in badroots:
608 for badroot in badroots:
609 pendings = [p for p in pendings if p[2] != badroot
609 pendings = [p for p in pendings if p[2] != badroot
610 and not p[2].startswith(badroot + '/')]
610 and not p[2].startswith(badroot + '/')]
611
611
612 # Tell tag renamings from tag creations
612 # Tell tag renamings from tag creations
613 renamings = []
613 renamings = []
614 for source, sourcerev, dest in pendings:
614 for source, sourcerev, dest in pendings:
615 tagname = dest.split('/')[-1]
615 tagname = dest.split('/')[-1]
616 if source.startswith(srctagspath):
616 if source.startswith(srctagspath):
617 renamings.append([source, sourcerev, tagname])
617 renamings.append([source, sourcerev, tagname])
618 continue
618 continue
619 if tagname in tags:
619 if tagname in tags:
620 # Keep the latest tag value
620 # Keep the latest tag value
621 continue
621 continue
622 # From revision may be fake, get one with changes
622 # From revision may be fake, get one with changes
623 try:
623 try:
624 tagid = self.latest(source, sourcerev)
624 tagid = self.latest(source, sourcerev)
625 if tagid and tagname not in tags:
625 if tagid and tagname not in tags:
626 tags[tagname] = tagid
626 tags[tagname] = tagid
627 except SvnPathNotFound:
627 except SvnPathNotFound:
628 # It happens when we are following directories
628 # It happens when we are following directories
629 # we assumed were copied with their parents
629 # we assumed were copied with their parents
630 # but were really created in the tag
630 # but were really created in the tag
631 # directory.
631 # directory.
632 pass
632 pass
633 pendings = renamings
633 pendings = renamings
634 tagspath = srctagspath
634 tagspath = srctagspath
635 finally:
635 finally:
636 stream.close()
636 stream.close()
637 return tags
637 return tags
638
638
639 def converted(self, rev, destrev):
639 def converted(self, rev, destrev):
640 if not self.wc:
640 if not self.wc:
641 return
641 return
642 if self.convertfp is None:
642 if self.convertfp is None:
643 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
643 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
644 'a')
644 'a')
645 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
645 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
646 self.convertfp.flush()
646 self.convertfp.flush()
647
647
648 def revid(self, revnum, module=None):
648 def revid(self, revnum, module=None):
649 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
649 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
650
650
651 def revnum(self, rev):
651 def revnum(self, rev):
652 return int(rev.split('@')[-1])
652 return int(rev.split('@')[-1])
653
653
654 def latest(self, path, stop=None):
654 def latest(self, path, stop=None):
655 """Find the latest revid affecting path, up to stop revision
655 """Find the latest revid affecting path, up to stop revision
656 number. If stop is None, default to repository latest
656 number. If stop is None, default to repository latest
657 revision. It may return a revision in a different module,
657 revision. It may return a revision in a different module,
658 since a branch may be moved without a change being
658 since a branch may be moved without a change being
659 reported. Return None if computed module does not belong to
659 reported. Return None if computed module does not belong to
660 rootmodule subtree.
660 rootmodule subtree.
661 """
661 """
662 def findchanges(path, start, stop=None):
662 def findchanges(path, start, stop=None):
663 stream = self._getlog([path], start, stop or 1)
663 stream = self._getlog([path], start, stop or 1)
664 try:
664 try:
665 for entry in stream:
665 for entry in stream:
666 paths, revnum, author, date, message = entry
666 paths, revnum, author, date, message = entry
667 if stop is None and paths:
667 if stop is None and paths:
668 # We do not know the latest changed revision,
668 # We do not know the latest changed revision,
669 # keep the first one with changed paths.
669 # keep the first one with changed paths.
670 break
670 break
671 if revnum <= stop:
671 if revnum <= stop:
672 break
672 break
673
673
674 for p in paths:
674 for p in paths:
675 if (not path.startswith(p) or
675 if (not path.startswith(p) or
676 not paths[p].copyfrom_path):
676 not paths[p].copyfrom_path):
677 continue
677 continue
678 newpath = paths[p].copyfrom_path + path[len(p):]
678 newpath = paths[p].copyfrom_path + path[len(p):]
679 self.ui.debug("branch renamed from %s to %s at %d\n" %
679 self.ui.debug("branch renamed from %s to %s at %d\n" %
680 (path, newpath, revnum))
680 (path, newpath, revnum))
681 path = newpath
681 path = newpath
682 break
682 break
683 if not paths:
683 if not paths:
684 revnum = None
684 revnum = None
685 return revnum, path
685 return revnum, path
686 finally:
686 finally:
687 stream.close()
687 stream.close()
688
688
689 if not path.startswith(self.rootmodule):
689 if not path.startswith(self.rootmodule):
690 # Requests on foreign branches may be forbidden at server level
690 # Requests on foreign branches may be forbidden at server level
691 self.ui.debug('ignoring foreign branch %r\n' % path)
691 self.ui.debug('ignoring foreign branch %r\n' % path)
692 return None
692 return None
693
693
694 if stop is None:
694 if stop is None:
695 stop = svn.ra.get_latest_revnum(self.ra)
695 stop = svn.ra.get_latest_revnum(self.ra)
696 try:
696 try:
697 prevmodule = self.reparent('')
697 prevmodule = self.reparent('')
698 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
698 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
699 self.reparent(prevmodule)
699 self.reparent(prevmodule)
700 except svn.core.SubversionException:
700 except svn.core.SubversionException:
701 dirent = None
701 dirent = None
702 if not dirent:
702 if not dirent:
703 raise SvnPathNotFound(_('%s not found up to revision %d')
703 raise SvnPathNotFound(_('%s not found up to revision %d')
704 % (path, stop))
704 % (path, stop))
705
705
706 # stat() gives us the previous revision on this line of
706 # stat() gives us the previous revision on this line of
707 # development, but it might be in *another module*. Fetch the
707 # development, but it might be in *another module*. Fetch the
708 # log and detect renames down to the latest revision.
708 # log and detect renames down to the latest revision.
709 revnum, realpath = findchanges(path, stop, dirent.created_rev)
709 revnum, realpath = findchanges(path, stop, dirent.created_rev)
710 if revnum is None:
710 if revnum is None:
711 # Tools like svnsync can create empty revision, when
711 # Tools like svnsync can create empty revision, when
712 # synchronizing only a subtree for instance. These empty
712 # synchronizing only a subtree for instance. These empty
713 # revisions created_rev still have their original values
713 # revisions created_rev still have their original values
714 # despite all changes having disappeared and can be
714 # despite all changes having disappeared and can be
715 # returned by ra.stat(), at least when stating the root
715 # returned by ra.stat(), at least when stating the root
716 # module. In that case, do not trust created_rev and scan
716 # module. In that case, do not trust created_rev and scan
717 # the whole history.
717 # the whole history.
718 revnum, realpath = findchanges(path, stop)
718 revnum, realpath = findchanges(path, stop)
719 if revnum is None:
719 if revnum is None:
720 self.ui.debug('ignoring empty branch %r\n' % realpath)
720 self.ui.debug('ignoring empty branch %r\n' % realpath)
721 return None
721 return None
722
722
723 if not realpath.startswith(self.rootmodule):
723 if not realpath.startswith(self.rootmodule):
724 self.ui.debug('ignoring foreign branch %r\n' % realpath)
724 self.ui.debug('ignoring foreign branch %r\n' % realpath)
725 return None
725 return None
726 return self.revid(revnum, realpath)
726 return self.revid(revnum, realpath)
727
727
728 def reparent(self, module):
728 def reparent(self, module):
729 """Reparent the svn transport and return the previous parent."""
729 """Reparent the svn transport and return the previous parent."""
730 if self.prevmodule == module:
730 if self.prevmodule == module:
731 return module
731 return module
732 svnurl = self.baseurl + quote(module)
732 svnurl = self.baseurl + quote(module)
733 prevmodule = self.prevmodule
733 prevmodule = self.prevmodule
734 if prevmodule is None:
734 if prevmodule is None:
735 prevmodule = ''
735 prevmodule = ''
736 self.ui.debug("reparent to %s\n" % svnurl)
736 self.ui.debug("reparent to %s\n" % svnurl)
737 svn.ra.reparent(self.ra, svnurl)
737 svn.ra.reparent(self.ra, svnurl)
738 self.prevmodule = module
738 self.prevmodule = module
739 return prevmodule
739 return prevmodule
740
740
741 def expandpaths(self, rev, paths, parents):
741 def expandpaths(self, rev, paths, parents):
742 changed, removed = set(), set()
742 changed, removed = set(), set()
743 copies = {}
743 copies = {}
744
744
745 new_module, revnum = revsplit(rev)[1:]
745 new_module, revnum = revsplit(rev)[1:]
746 if new_module != self.module:
746 if new_module != self.module:
747 self.module = new_module
747 self.module = new_module
748 self.reparent(self.module)
748 self.reparent(self.module)
749
749
750 for i, (path, ent) in enumerate(paths):
750 for i, (path, ent) in enumerate(paths):
751 self.ui.progress(_('scanning paths'), i, item=path,
751 self.ui.progress(_('scanning paths'), i, item=path,
752 total=len(paths))
752 total=len(paths), unit=_('paths'))
753 entrypath = self.getrelpath(path)
753 entrypath = self.getrelpath(path)
754
754
755 kind = self._checkpath(entrypath, revnum)
755 kind = self._checkpath(entrypath, revnum)
756 if kind == svn.core.svn_node_file:
756 if kind == svn.core.svn_node_file:
757 changed.add(self.recode(entrypath))
757 changed.add(self.recode(entrypath))
758 if not ent.copyfrom_path or not parents:
758 if not ent.copyfrom_path or not parents:
759 continue
759 continue
760 # Copy sources not in parent revisions cannot be
760 # Copy sources not in parent revisions cannot be
761 # represented, ignore their origin for now
761 # represented, ignore their origin for now
762 pmodule, prevnum = revsplit(parents[0])[1:]
762 pmodule, prevnum = revsplit(parents[0])[1:]
763 if ent.copyfrom_rev < prevnum:
763 if ent.copyfrom_rev < prevnum:
764 continue
764 continue
765 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
765 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
766 if not copyfrom_path:
766 if not copyfrom_path:
767 continue
767 continue
768 self.ui.debug("copied to %s from %s@%s\n" %
768 self.ui.debug("copied to %s from %s@%s\n" %
769 (entrypath, copyfrom_path, ent.copyfrom_rev))
769 (entrypath, copyfrom_path, ent.copyfrom_rev))
770 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
770 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
771 elif kind == 0: # gone, but had better be a deleted *file*
771 elif kind == 0: # gone, but had better be a deleted *file*
772 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
772 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
773 pmodule, prevnum = revsplit(parents[0])[1:]
773 pmodule, prevnum = revsplit(parents[0])[1:]
774 parentpath = pmodule + "/" + entrypath
774 parentpath = pmodule + "/" + entrypath
775 fromkind = self._checkpath(entrypath, prevnum, pmodule)
775 fromkind = self._checkpath(entrypath, prevnum, pmodule)
776
776
777 if fromkind == svn.core.svn_node_file:
777 if fromkind == svn.core.svn_node_file:
778 removed.add(self.recode(entrypath))
778 removed.add(self.recode(entrypath))
779 elif fromkind == svn.core.svn_node_dir:
779 elif fromkind == svn.core.svn_node_dir:
780 oroot = parentpath.strip('/')
780 oroot = parentpath.strip('/')
781 nroot = path.strip('/')
781 nroot = path.strip('/')
782 children = self._iterfiles(oroot, prevnum)
782 children = self._iterfiles(oroot, prevnum)
783 for childpath in children:
783 for childpath in children:
784 childpath = childpath.replace(oroot, nroot)
784 childpath = childpath.replace(oroot, nroot)
785 childpath = self.getrelpath("/" + childpath, pmodule)
785 childpath = self.getrelpath("/" + childpath, pmodule)
786 if childpath:
786 if childpath:
787 removed.add(self.recode(childpath))
787 removed.add(self.recode(childpath))
788 else:
788 else:
789 self.ui.debug('unknown path in revision %d: %s\n' % \
789 self.ui.debug('unknown path in revision %d: %s\n' % \
790 (revnum, path))
790 (revnum, path))
791 elif kind == svn.core.svn_node_dir:
791 elif kind == svn.core.svn_node_dir:
792 if ent.action == 'M':
792 if ent.action == 'M':
793 # If the directory just had a prop change,
793 # If the directory just had a prop change,
794 # then we shouldn't need to look for its children.
794 # then we shouldn't need to look for its children.
795 continue
795 continue
796 if ent.action == 'R' and parents:
796 if ent.action == 'R' and parents:
797 # If a directory is replacing a file, mark the previous
797 # If a directory is replacing a file, mark the previous
798 # file as deleted
798 # file as deleted
799 pmodule, prevnum = revsplit(parents[0])[1:]
799 pmodule, prevnum = revsplit(parents[0])[1:]
800 pkind = self._checkpath(entrypath, prevnum, pmodule)
800 pkind = self._checkpath(entrypath, prevnum, pmodule)
801 if pkind == svn.core.svn_node_file:
801 if pkind == svn.core.svn_node_file:
802 removed.add(self.recode(entrypath))
802 removed.add(self.recode(entrypath))
803 elif pkind == svn.core.svn_node_dir:
803 elif pkind == svn.core.svn_node_dir:
804 # We do not know what files were kept or removed,
804 # We do not know what files were kept or removed,
805 # mark them all as changed.
805 # mark them all as changed.
806 for childpath in self._iterfiles(pmodule, prevnum):
806 for childpath in self._iterfiles(pmodule, prevnum):
807 childpath = self.getrelpath("/" + childpath)
807 childpath = self.getrelpath("/" + childpath)
808 if childpath:
808 if childpath:
809 changed.add(self.recode(childpath))
809 changed.add(self.recode(childpath))
810
810
811 for childpath in self._iterfiles(path, revnum):
811 for childpath in self._iterfiles(path, revnum):
812 childpath = self.getrelpath("/" + childpath)
812 childpath = self.getrelpath("/" + childpath)
813 if childpath:
813 if childpath:
814 changed.add(self.recode(childpath))
814 changed.add(self.recode(childpath))
815
815
816 # Handle directory copies
816 # Handle directory copies
817 if not ent.copyfrom_path or not parents:
817 if not ent.copyfrom_path or not parents:
818 continue
818 continue
819 # Copy sources not in parent revisions cannot be
819 # Copy sources not in parent revisions cannot be
820 # represented, ignore their origin for now
820 # represented, ignore their origin for now
821 pmodule, prevnum = revsplit(parents[0])[1:]
821 pmodule, prevnum = revsplit(parents[0])[1:]
822 if ent.copyfrom_rev < prevnum:
822 if ent.copyfrom_rev < prevnum:
823 continue
823 continue
824 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
824 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
825 if not copyfrompath:
825 if not copyfrompath:
826 continue
826 continue
827 self.ui.debug("mark %s came from %s:%d\n"
827 self.ui.debug("mark %s came from %s:%d\n"
828 % (path, copyfrompath, ent.copyfrom_rev))
828 % (path, copyfrompath, ent.copyfrom_rev))
829 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
829 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
830 for childpath in children:
830 for childpath in children:
831 childpath = self.getrelpath("/" + childpath, pmodule)
831 childpath = self.getrelpath("/" + childpath, pmodule)
832 if not childpath:
832 if not childpath:
833 continue
833 continue
834 copytopath = path + childpath[len(copyfrompath):]
834 copytopath = path + childpath[len(copyfrompath):]
835 copytopath = self.getrelpath(copytopath)
835 copytopath = self.getrelpath(copytopath)
836 copies[self.recode(copytopath)] = self.recode(childpath)
836 copies[self.recode(copytopath)] = self.recode(childpath)
837
837
838 self.ui.progress(_('scanning paths'), None)
838 self.ui.progress(_('scanning paths'), None)
839 changed.update(removed)
839 changed.update(removed)
840 return (list(changed), removed, copies)
840 return (list(changed), removed, copies)
841
841
842 def _fetch_revisions(self, from_revnum, to_revnum):
842 def _fetch_revisions(self, from_revnum, to_revnum):
843 if from_revnum < to_revnum:
843 if from_revnum < to_revnum:
844 from_revnum, to_revnum = to_revnum, from_revnum
844 from_revnum, to_revnum = to_revnum, from_revnum
845
845
846 self.child_cset = None
846 self.child_cset = None
847
847
848 def parselogentry(orig_paths, revnum, author, date, message):
848 def parselogentry(orig_paths, revnum, author, date, message):
849 """Return the parsed commit object or None, and True if
849 """Return the parsed commit object or None, and True if
850 the revision is a branch root.
850 the revision is a branch root.
851 """
851 """
852 self.ui.debug("parsing revision %d (%d changes)\n" %
852 self.ui.debug("parsing revision %d (%d changes)\n" %
853 (revnum, len(orig_paths)))
853 (revnum, len(orig_paths)))
854
854
855 branched = False
855 branched = False
856 rev = self.revid(revnum)
856 rev = self.revid(revnum)
857 # branch log might return entries for a parent we already have
857 # branch log might return entries for a parent we already have
858
858
859 if rev in self.commits or revnum < to_revnum:
859 if rev in self.commits or revnum < to_revnum:
860 return None, branched
860 return None, branched
861
861
862 parents = []
862 parents = []
863 # check whether this revision is the start of a branch or part
863 # check whether this revision is the start of a branch or part
864 # of a branch renaming
864 # of a branch renaming
865 orig_paths = sorted(orig_paths.iteritems())
865 orig_paths = sorted(orig_paths.iteritems())
866 root_paths = [(p, e) for p, e in orig_paths
866 root_paths = [(p, e) for p, e in orig_paths
867 if self.module.startswith(p)]
867 if self.module.startswith(p)]
868 if root_paths:
868 if root_paths:
869 path, ent = root_paths[-1]
869 path, ent = root_paths[-1]
870 if ent.copyfrom_path:
870 if ent.copyfrom_path:
871 branched = True
871 branched = True
872 newpath = ent.copyfrom_path + self.module[len(path):]
872 newpath = ent.copyfrom_path + self.module[len(path):]
873 # ent.copyfrom_rev may not be the actual last revision
873 # ent.copyfrom_rev may not be the actual last revision
874 previd = self.latest(newpath, ent.copyfrom_rev)
874 previd = self.latest(newpath, ent.copyfrom_rev)
875 if previd is not None:
875 if previd is not None:
876 prevmodule, prevnum = revsplit(previd)[1:]
876 prevmodule, prevnum = revsplit(previd)[1:]
877 if prevnum >= self.startrev:
877 if prevnum >= self.startrev:
878 parents = [previd]
878 parents = [previd]
879 self.ui.note(
879 self.ui.note(
880 _('found parent of branch %s at %d: %s\n') %
880 _('found parent of branch %s at %d: %s\n') %
881 (self.module, prevnum, prevmodule))
881 (self.module, prevnum, prevmodule))
882 else:
882 else:
883 self.ui.debug("no copyfrom path, don't know what to do.\n")
883 self.ui.debug("no copyfrom path, don't know what to do.\n")
884
884
885 paths = []
885 paths = []
886 # filter out unrelated paths
886 # filter out unrelated paths
887 for path, ent in orig_paths:
887 for path, ent in orig_paths:
888 if self.getrelpath(path) is None:
888 if self.getrelpath(path) is None:
889 continue
889 continue
890 paths.append((path, ent))
890 paths.append((path, ent))
891
891
892 # Example SVN datetime. Includes microseconds.
892 # Example SVN datetime. Includes microseconds.
893 # ISO-8601 conformant
893 # ISO-8601 conformant
894 # '2007-01-04T17:35:00.902377Z'
894 # '2007-01-04T17:35:00.902377Z'
895 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
895 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
896 if self.ui.configbool('convert', 'localtimezone'):
896 if self.ui.configbool('convert', 'localtimezone'):
897 date = makedatetimestamp(date[0])
897 date = makedatetimestamp(date[0])
898
898
899 if message:
899 if message:
900 log = self.recode(message)
900 log = self.recode(message)
901 else:
901 else:
902 log = ''
902 log = ''
903
903
904 if author:
904 if author:
905 author = self.recode(author)
905 author = self.recode(author)
906 else:
906 else:
907 author = ''
907 author = ''
908
908
909 try:
909 try:
910 branch = self.module.split("/")[-1]
910 branch = self.module.split("/")[-1]
911 if branch == self.trunkname:
911 if branch == self.trunkname:
912 branch = None
912 branch = None
913 except IndexError:
913 except IndexError:
914 branch = None
914 branch = None
915
915
916 cset = commit(author=author,
916 cset = commit(author=author,
917 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
917 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
918 desc=log,
918 desc=log,
919 parents=parents,
919 parents=parents,
920 branch=branch,
920 branch=branch,
921 rev=rev)
921 rev=rev)
922
922
923 self.commits[rev] = cset
923 self.commits[rev] = cset
924 # The parents list is *shared* among self.paths and the
924 # The parents list is *shared* among self.paths and the
925 # commit object. Both will be updated below.
925 # commit object. Both will be updated below.
926 self.paths[rev] = (paths, cset.parents)
926 self.paths[rev] = (paths, cset.parents)
927 if self.child_cset and not self.child_cset.parents:
927 if self.child_cset and not self.child_cset.parents:
928 self.child_cset.parents[:] = [rev]
928 self.child_cset.parents[:] = [rev]
929 self.child_cset = cset
929 self.child_cset = cset
930 return cset, branched
930 return cset, branched
931
931
932 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
932 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
933 (self.module, from_revnum, to_revnum))
933 (self.module, from_revnum, to_revnum))
934
934
935 try:
935 try:
936 firstcset = None
936 firstcset = None
937 lastonbranch = False
937 lastonbranch = False
938 stream = self._getlog([self.module], from_revnum, to_revnum)
938 stream = self._getlog([self.module], from_revnum, to_revnum)
939 try:
939 try:
940 for entry in stream:
940 for entry in stream:
941 paths, revnum, author, date, message = entry
941 paths, revnum, author, date, message = entry
942 if revnum < self.startrev:
942 if revnum < self.startrev:
943 lastonbranch = True
943 lastonbranch = True
944 break
944 break
945 if not paths:
945 if not paths:
946 self.ui.debug('revision %d has no entries\n' % revnum)
946 self.ui.debug('revision %d has no entries\n' % revnum)
947 # If we ever leave the loop on an empty
947 # If we ever leave the loop on an empty
948 # revision, do not try to get a parent branch
948 # revision, do not try to get a parent branch
949 lastonbranch = lastonbranch or revnum == 0
949 lastonbranch = lastonbranch or revnum == 0
950 continue
950 continue
951 cset, lastonbranch = parselogentry(paths, revnum, author,
951 cset, lastonbranch = parselogentry(paths, revnum, author,
952 date, message)
952 date, message)
953 if cset:
953 if cset:
954 firstcset = cset
954 firstcset = cset
955 if lastonbranch:
955 if lastonbranch:
956 break
956 break
957 finally:
957 finally:
958 stream.close()
958 stream.close()
959
959
960 if not lastonbranch and firstcset and not firstcset.parents:
960 if not lastonbranch and firstcset and not firstcset.parents:
961 # The first revision of the sequence (the last fetched one)
961 # The first revision of the sequence (the last fetched one)
962 # has invalid parents if not a branch root. Find the parent
962 # has invalid parents if not a branch root. Find the parent
963 # revision now, if any.
963 # revision now, if any.
964 try:
964 try:
965 firstrevnum = self.revnum(firstcset.rev)
965 firstrevnum = self.revnum(firstcset.rev)
966 if firstrevnum > 1:
966 if firstrevnum > 1:
967 latest = self.latest(self.module, firstrevnum - 1)
967 latest = self.latest(self.module, firstrevnum - 1)
968 if latest:
968 if latest:
969 firstcset.parents.append(latest)
969 firstcset.parents.append(latest)
970 except SvnPathNotFound:
970 except SvnPathNotFound:
971 pass
971 pass
972 except svn.core.SubversionException as xxx_todo_changeme:
972 except svn.core.SubversionException as xxx_todo_changeme:
973 (inst, num) = xxx_todo_changeme.args
973 (inst, num) = xxx_todo_changeme.args
974 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
974 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
975 raise error.Abort(_('svn: branch has no revision %s')
975 raise error.Abort(_('svn: branch has no revision %s')
976 % to_revnum)
976 % to_revnum)
977 raise
977 raise
978
978
979 def getfile(self, file, rev):
979 def getfile(self, file, rev):
980 # TODO: ra.get_file transmits the whole file instead of diffs.
980 # TODO: ra.get_file transmits the whole file instead of diffs.
981 if file in self.removed:
981 if file in self.removed:
982 return None, None
982 return None, None
983 mode = ''
983 mode = ''
984 try:
984 try:
985 new_module, revnum = revsplit(rev)[1:]
985 new_module, revnum = revsplit(rev)[1:]
986 if self.module != new_module:
986 if self.module != new_module:
987 self.module = new_module
987 self.module = new_module
988 self.reparent(self.module)
988 self.reparent(self.module)
989 io = StringIO()
989 io = StringIO()
990 info = svn.ra.get_file(self.ra, file, revnum, io)
990 info = svn.ra.get_file(self.ra, file, revnum, io)
991 data = io.getvalue()
991 data = io.getvalue()
992 # ra.get_file() seems to keep a reference on the input buffer
992 # ra.get_file() seems to keep a reference on the input buffer
993 # preventing collection. Release it explicitly.
993 # preventing collection. Release it explicitly.
994 io.close()
994 io.close()
995 if isinstance(info, list):
995 if isinstance(info, list):
996 info = info[-1]
996 info = info[-1]
997 mode = ("svn:executable" in info) and 'x' or ''
997 mode = ("svn:executable" in info) and 'x' or ''
998 mode = ("svn:special" in info) and 'l' or mode
998 mode = ("svn:special" in info) and 'l' or mode
999 except svn.core.SubversionException as e:
999 except svn.core.SubversionException as e:
1000 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
1000 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
1001 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
1001 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
1002 if e.apr_err in notfound: # File not found
1002 if e.apr_err in notfound: # File not found
1003 return None, None
1003 return None, None
1004 raise
1004 raise
1005 if mode == 'l':
1005 if mode == 'l':
1006 link_prefix = "link "
1006 link_prefix = "link "
1007 if data.startswith(link_prefix):
1007 if data.startswith(link_prefix):
1008 data = data[len(link_prefix):]
1008 data = data[len(link_prefix):]
1009 return data, mode
1009 return data, mode
1010
1010
1011 def _iterfiles(self, path, revnum):
1011 def _iterfiles(self, path, revnum):
1012 """Enumerate all files in path at revnum, recursively."""
1012 """Enumerate all files in path at revnum, recursively."""
1013 path = path.strip('/')
1013 path = path.strip('/')
1014 pool = svn.core.Pool()
1014 pool = svn.core.Pool()
1015 rpath = '/'.join([self.baseurl, quote(path)]).strip('/')
1015 rpath = '/'.join([self.baseurl, quote(path)]).strip('/')
1016 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
1016 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
1017 if path:
1017 if path:
1018 path += '/'
1018 path += '/'
1019 return ((path + p) for p, e in entries.iteritems()
1019 return ((path + p) for p, e in entries.iteritems()
1020 if e.kind == svn.core.svn_node_file)
1020 if e.kind == svn.core.svn_node_file)
1021
1021
1022 def getrelpath(self, path, module=None):
1022 def getrelpath(self, path, module=None):
1023 if module is None:
1023 if module is None:
1024 module = self.module
1024 module = self.module
1025 # Given the repository url of this wc, say
1025 # Given the repository url of this wc, say
1026 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
1026 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
1027 # extract the "entry" portion (a relative path) from what
1027 # extract the "entry" portion (a relative path) from what
1028 # svn log --xml says, i.e.
1028 # svn log --xml says, i.e.
1029 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
1029 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
1030 # that is to say "tests/PloneTestCase.py"
1030 # that is to say "tests/PloneTestCase.py"
1031 if path.startswith(module):
1031 if path.startswith(module):
1032 relative = path.rstrip('/')[len(module):]
1032 relative = path.rstrip('/')[len(module):]
1033 if relative.startswith('/'):
1033 if relative.startswith('/'):
1034 return relative[1:]
1034 return relative[1:]
1035 elif relative == '':
1035 elif relative == '':
1036 return relative
1036 return relative
1037
1037
1038 # The path is outside our tracked tree...
1038 # The path is outside our tracked tree...
1039 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
1039 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
1040 return None
1040 return None
1041
1041
1042 def _checkpath(self, path, revnum, module=None):
1042 def _checkpath(self, path, revnum, module=None):
1043 if module is not None:
1043 if module is not None:
1044 prevmodule = self.reparent('')
1044 prevmodule = self.reparent('')
1045 path = module + '/' + path
1045 path = module + '/' + path
1046 try:
1046 try:
1047 # ra.check_path does not like leading slashes very much, it leads
1047 # ra.check_path does not like leading slashes very much, it leads
1048 # to PROPFIND subversion errors
1048 # to PROPFIND subversion errors
1049 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
1049 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
1050 finally:
1050 finally:
1051 if module is not None:
1051 if module is not None:
1052 self.reparent(prevmodule)
1052 self.reparent(prevmodule)
1053
1053
1054 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
1054 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
1055 strict_node_history=False):
1055 strict_node_history=False):
1056 # Normalize path names, svn >= 1.5 only wants paths relative to
1056 # Normalize path names, svn >= 1.5 only wants paths relative to
1057 # supplied URL
1057 # supplied URL
1058 relpaths = []
1058 relpaths = []
1059 for p in paths:
1059 for p in paths:
1060 if not p.startswith('/'):
1060 if not p.startswith('/'):
1061 p = self.module + '/' + p
1061 p = self.module + '/' + p
1062 relpaths.append(p.strip('/'))
1062 relpaths.append(p.strip('/'))
1063 args = [self.baseurl, relpaths, start, end, limit,
1063 args = [self.baseurl, relpaths, start, end, limit,
1064 discover_changed_paths, strict_node_history]
1064 discover_changed_paths, strict_node_history]
1065 # developer config: convert.svn.debugsvnlog
1065 # developer config: convert.svn.debugsvnlog
1066 if not self.ui.configbool('convert', 'svn.debugsvnlog', True):
1066 if not self.ui.configbool('convert', 'svn.debugsvnlog', True):
1067 return directlogstream(*args)
1067 return directlogstream(*args)
1068 arg = encodeargs(args)
1068 arg = encodeargs(args)
1069 hgexe = util.hgexecutable()
1069 hgexe = util.hgexecutable()
1070 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
1070 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
1071 stdin, stdout = util.popen2(util.quotecommand(cmd))
1071 stdin, stdout = util.popen2(util.quotecommand(cmd))
1072 stdin.write(arg)
1072 stdin.write(arg)
1073 try:
1073 try:
1074 stdin.close()
1074 stdin.close()
1075 except IOError:
1075 except IOError:
1076 raise error.Abort(_('Mercurial failed to run itself, check'
1076 raise error.Abort(_('Mercurial failed to run itself, check'
1077 ' hg executable is in PATH'))
1077 ' hg executable is in PATH'))
1078 return logstream(stdout)
1078 return logstream(stdout)
1079
1079
1080 pre_revprop_change = '''#!/bin/sh
1080 pre_revprop_change = '''#!/bin/sh
1081
1081
1082 REPOS="$1"
1082 REPOS="$1"
1083 REV="$2"
1083 REV="$2"
1084 USER="$3"
1084 USER="$3"
1085 PROPNAME="$4"
1085 PROPNAME="$4"
1086 ACTION="$5"
1086 ACTION="$5"
1087
1087
1088 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
1088 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
1089 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
1089 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
1090 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
1090 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
1091
1091
1092 echo "Changing prohibited revision property" >&2
1092 echo "Changing prohibited revision property" >&2
1093 exit 1
1093 exit 1
1094 '''
1094 '''
1095
1095
1096 class svn_sink(converter_sink, commandline):
1096 class svn_sink(converter_sink, commandline):
1097 commit_re = re.compile(r'Committed revision (\d+).', re.M)
1097 commit_re = re.compile(r'Committed revision (\d+).', re.M)
1098 uuid_re = re.compile(r'Repository UUID:\s*(\S+)', re.M)
1098 uuid_re = re.compile(r'Repository UUID:\s*(\S+)', re.M)
1099
1099
1100 def prerun(self):
1100 def prerun(self):
1101 if self.wc:
1101 if self.wc:
1102 os.chdir(self.wc)
1102 os.chdir(self.wc)
1103
1103
1104 def postrun(self):
1104 def postrun(self):
1105 if self.wc:
1105 if self.wc:
1106 os.chdir(self.cwd)
1106 os.chdir(self.cwd)
1107
1107
1108 def join(self, name):
1108 def join(self, name):
1109 return os.path.join(self.wc, '.svn', name)
1109 return os.path.join(self.wc, '.svn', name)
1110
1110
1111 def revmapfile(self):
1111 def revmapfile(self):
1112 return self.join('hg-shamap')
1112 return self.join('hg-shamap')
1113
1113
1114 def authorfile(self):
1114 def authorfile(self):
1115 return self.join('hg-authormap')
1115 return self.join('hg-authormap')
1116
1116
1117 def __init__(self, ui, path):
1117 def __init__(self, ui, path):
1118
1118
1119 converter_sink.__init__(self, ui, path)
1119 converter_sink.__init__(self, ui, path)
1120 commandline.__init__(self, ui, 'svn')
1120 commandline.__init__(self, ui, 'svn')
1121 self.delete = []
1121 self.delete = []
1122 self.setexec = []
1122 self.setexec = []
1123 self.delexec = []
1123 self.delexec = []
1124 self.copies = []
1124 self.copies = []
1125 self.wc = None
1125 self.wc = None
1126 self.cwd = os.getcwd()
1126 self.cwd = os.getcwd()
1127
1127
1128 created = False
1128 created = False
1129 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
1129 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
1130 self.wc = os.path.realpath(path)
1130 self.wc = os.path.realpath(path)
1131 self.run0('update')
1131 self.run0('update')
1132 else:
1132 else:
1133 if not re.search(r'^(file|http|https|svn|svn\+ssh)\://', path):
1133 if not re.search(r'^(file|http|https|svn|svn\+ssh)\://', path):
1134 path = os.path.realpath(path)
1134 path = os.path.realpath(path)
1135 if os.path.isdir(os.path.dirname(path)):
1135 if os.path.isdir(os.path.dirname(path)):
1136 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
1136 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
1137 ui.status(_('initializing svn repository %r\n') %
1137 ui.status(_('initializing svn repository %r\n') %
1138 os.path.basename(path))
1138 os.path.basename(path))
1139 commandline(ui, 'svnadmin').run0('create', path)
1139 commandline(ui, 'svnadmin').run0('create', path)
1140 created = path
1140 created = path
1141 path = util.normpath(path)
1141 path = util.normpath(path)
1142 if not path.startswith('/'):
1142 if not path.startswith('/'):
1143 path = '/' + path
1143 path = '/' + path
1144 path = 'file://' + path
1144 path = 'file://' + path
1145
1145
1146 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
1146 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
1147 ui.status(_('initializing svn working copy %r\n')
1147 ui.status(_('initializing svn working copy %r\n')
1148 % os.path.basename(wcpath))
1148 % os.path.basename(wcpath))
1149 self.run0('checkout', path, wcpath)
1149 self.run0('checkout', path, wcpath)
1150
1150
1151 self.wc = wcpath
1151 self.wc = wcpath
1152 self.opener = scmutil.opener(self.wc)
1152 self.opener = scmutil.opener(self.wc)
1153 self.wopener = scmutil.opener(self.wc)
1153 self.wopener = scmutil.opener(self.wc)
1154 self.childmap = mapfile(ui, self.join('hg-childmap'))
1154 self.childmap = mapfile(ui, self.join('hg-childmap'))
1155 if util.checkexec(self.wc):
1155 if util.checkexec(self.wc):
1156 self.is_exec = util.isexec
1156 self.is_exec = util.isexec
1157 else:
1157 else:
1158 self.is_exec = None
1158 self.is_exec = None
1159
1159
1160 if created:
1160 if created:
1161 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1161 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1162 fp = open(hook, 'w')
1162 fp = open(hook, 'w')
1163 fp.write(pre_revprop_change)
1163 fp.write(pre_revprop_change)
1164 fp.close()
1164 fp.close()
1165 util.setflags(hook, False, True)
1165 util.setflags(hook, False, True)
1166
1166
1167 output = self.run0('info')
1167 output = self.run0('info')
1168 self.uuid = self.uuid_re.search(output).group(1).strip()
1168 self.uuid = self.uuid_re.search(output).group(1).strip()
1169
1169
1170 def wjoin(self, *names):
1170 def wjoin(self, *names):
1171 return os.path.join(self.wc, *names)
1171 return os.path.join(self.wc, *names)
1172
1172
1173 @propertycache
1173 @propertycache
1174 def manifest(self):
1174 def manifest(self):
1175 # As of svn 1.7, the "add" command fails when receiving
1175 # As of svn 1.7, the "add" command fails when receiving
1176 # already tracked entries, so we have to track and filter them
1176 # already tracked entries, so we have to track and filter them
1177 # ourselves.
1177 # ourselves.
1178 m = set()
1178 m = set()
1179 output = self.run0('ls', recursive=True, xml=True)
1179 output = self.run0('ls', recursive=True, xml=True)
1180 doc = xml.dom.minidom.parseString(output)
1180 doc = xml.dom.minidom.parseString(output)
1181 for e in doc.getElementsByTagName('entry'):
1181 for e in doc.getElementsByTagName('entry'):
1182 for n in e.childNodes:
1182 for n in e.childNodes:
1183 if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
1183 if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
1184 continue
1184 continue
1185 name = ''.join(c.data for c in n.childNodes
1185 name = ''.join(c.data for c in n.childNodes
1186 if c.nodeType == c.TEXT_NODE)
1186 if c.nodeType == c.TEXT_NODE)
1187 # Entries are compared with names coming from
1187 # Entries are compared with names coming from
1188 # mercurial, so bytes with undefined encoding. Our
1188 # mercurial, so bytes with undefined encoding. Our
1189 # best bet is to assume they are in local
1189 # best bet is to assume they are in local
1190 # encoding. They will be passed to command line calls
1190 # encoding. They will be passed to command line calls
1191 # later anyway, so they better be.
1191 # later anyway, so they better be.
1192 m.add(encoding.tolocal(name.encode('utf-8')))
1192 m.add(encoding.tolocal(name.encode('utf-8')))
1193 break
1193 break
1194 return m
1194 return m
1195
1195
1196 def putfile(self, filename, flags, data):
1196 def putfile(self, filename, flags, data):
1197 if 'l' in flags:
1197 if 'l' in flags:
1198 self.wopener.symlink(data, filename)
1198 self.wopener.symlink(data, filename)
1199 else:
1199 else:
1200 try:
1200 try:
1201 if os.path.islink(self.wjoin(filename)):
1201 if os.path.islink(self.wjoin(filename)):
1202 os.unlink(filename)
1202 os.unlink(filename)
1203 except OSError:
1203 except OSError:
1204 pass
1204 pass
1205 self.wopener.write(filename, data)
1205 self.wopener.write(filename, data)
1206
1206
1207 if self.is_exec:
1207 if self.is_exec:
1208 if self.is_exec(self.wjoin(filename)):
1208 if self.is_exec(self.wjoin(filename)):
1209 if 'x' not in flags:
1209 if 'x' not in flags:
1210 self.delexec.append(filename)
1210 self.delexec.append(filename)
1211 else:
1211 else:
1212 if 'x' in flags:
1212 if 'x' in flags:
1213 self.setexec.append(filename)
1213 self.setexec.append(filename)
1214 util.setflags(self.wjoin(filename), False, 'x' in flags)
1214 util.setflags(self.wjoin(filename), False, 'x' in flags)
1215
1215
1216 def _copyfile(self, source, dest):
1216 def _copyfile(self, source, dest):
1217 # SVN's copy command pukes if the destination file exists, but
1217 # SVN's copy command pukes if the destination file exists, but
1218 # our copyfile method expects to record a copy that has
1218 # our copyfile method expects to record a copy that has
1219 # already occurred. Cross the semantic gap.
1219 # already occurred. Cross the semantic gap.
1220 wdest = self.wjoin(dest)
1220 wdest = self.wjoin(dest)
1221 exists = os.path.lexists(wdest)
1221 exists = os.path.lexists(wdest)
1222 if exists:
1222 if exists:
1223 fd, tempname = tempfile.mkstemp(
1223 fd, tempname = tempfile.mkstemp(
1224 prefix='hg-copy-', dir=os.path.dirname(wdest))
1224 prefix='hg-copy-', dir=os.path.dirname(wdest))
1225 os.close(fd)
1225 os.close(fd)
1226 os.unlink(tempname)
1226 os.unlink(tempname)
1227 os.rename(wdest, tempname)
1227 os.rename(wdest, tempname)
1228 try:
1228 try:
1229 self.run0('copy', source, dest)
1229 self.run0('copy', source, dest)
1230 finally:
1230 finally:
1231 self.manifest.add(dest)
1231 self.manifest.add(dest)
1232 if exists:
1232 if exists:
1233 try:
1233 try:
1234 os.unlink(wdest)
1234 os.unlink(wdest)
1235 except OSError:
1235 except OSError:
1236 pass
1236 pass
1237 os.rename(tempname, wdest)
1237 os.rename(tempname, wdest)
1238
1238
1239 def dirs_of(self, files):
1239 def dirs_of(self, files):
1240 dirs = set()
1240 dirs = set()
1241 for f in files:
1241 for f in files:
1242 if os.path.isdir(self.wjoin(f)):
1242 if os.path.isdir(self.wjoin(f)):
1243 dirs.add(f)
1243 dirs.add(f)
1244 for i in strutil.rfindall(f, '/'):
1244 for i in strutil.rfindall(f, '/'):
1245 dirs.add(f[:i])
1245 dirs.add(f[:i])
1246 return dirs
1246 return dirs
1247
1247
1248 def add_dirs(self, files):
1248 def add_dirs(self, files):
1249 add_dirs = [d for d in sorted(self.dirs_of(files))
1249 add_dirs = [d for d in sorted(self.dirs_of(files))
1250 if d not in self.manifest]
1250 if d not in self.manifest]
1251 if add_dirs:
1251 if add_dirs:
1252 self.manifest.update(add_dirs)
1252 self.manifest.update(add_dirs)
1253 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1253 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1254 return add_dirs
1254 return add_dirs
1255
1255
1256 def add_files(self, files):
1256 def add_files(self, files):
1257 files = [f for f in files if f not in self.manifest]
1257 files = [f for f in files if f not in self.manifest]
1258 if files:
1258 if files:
1259 self.manifest.update(files)
1259 self.manifest.update(files)
1260 self.xargs(files, 'add', quiet=True)
1260 self.xargs(files, 'add', quiet=True)
1261 return files
1261 return files
1262
1262
1263 def addchild(self, parent, child):
1263 def addchild(self, parent, child):
1264 self.childmap[parent] = child
1264 self.childmap[parent] = child
1265
1265
1266 def revid(self, rev):
1266 def revid(self, rev):
1267 return u"svn:%s@%s" % (self.uuid, rev)
1267 return u"svn:%s@%s" % (self.uuid, rev)
1268
1268
1269 def putcommit(self, files, copies, parents, commit, source, revmap, full,
1269 def putcommit(self, files, copies, parents, commit, source, revmap, full,
1270 cleanp2):
1270 cleanp2):
1271 for parent in parents:
1271 for parent in parents:
1272 try:
1272 try:
1273 return self.revid(self.childmap[parent])
1273 return self.revid(self.childmap[parent])
1274 except KeyError:
1274 except KeyError:
1275 pass
1275 pass
1276
1276
1277 # Apply changes to working copy
1277 # Apply changes to working copy
1278 for f, v in files:
1278 for f, v in files:
1279 data, mode = source.getfile(f, v)
1279 data, mode = source.getfile(f, v)
1280 if data is None:
1280 if data is None:
1281 self.delete.append(f)
1281 self.delete.append(f)
1282 else:
1282 else:
1283 self.putfile(f, mode, data)
1283 self.putfile(f, mode, data)
1284 if f in copies:
1284 if f in copies:
1285 self.copies.append([copies[f], f])
1285 self.copies.append([copies[f], f])
1286 if full:
1286 if full:
1287 self.delete.extend(sorted(self.manifest.difference(files)))
1287 self.delete.extend(sorted(self.manifest.difference(files)))
1288 files = [f[0] for f in files]
1288 files = [f[0] for f in files]
1289
1289
1290 entries = set(self.delete)
1290 entries = set(self.delete)
1291 files = frozenset(files)
1291 files = frozenset(files)
1292 entries.update(self.add_dirs(files.difference(entries)))
1292 entries.update(self.add_dirs(files.difference(entries)))
1293 if self.copies:
1293 if self.copies:
1294 for s, d in self.copies:
1294 for s, d in self.copies:
1295 self._copyfile(s, d)
1295 self._copyfile(s, d)
1296 self.copies = []
1296 self.copies = []
1297 if self.delete:
1297 if self.delete:
1298 self.xargs(self.delete, 'delete')
1298 self.xargs(self.delete, 'delete')
1299 for f in self.delete:
1299 for f in self.delete:
1300 self.manifest.remove(f)
1300 self.manifest.remove(f)
1301 self.delete = []
1301 self.delete = []
1302 entries.update(self.add_files(files.difference(entries)))
1302 entries.update(self.add_files(files.difference(entries)))
1303 if self.delexec:
1303 if self.delexec:
1304 self.xargs(self.delexec, 'propdel', 'svn:executable')
1304 self.xargs(self.delexec, 'propdel', 'svn:executable')
1305 self.delexec = []
1305 self.delexec = []
1306 if self.setexec:
1306 if self.setexec:
1307 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1307 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1308 self.setexec = []
1308 self.setexec = []
1309
1309
1310 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1310 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1311 fp = os.fdopen(fd, 'w')
1311 fp = os.fdopen(fd, 'w')
1312 fp.write(commit.desc)
1312 fp.write(commit.desc)
1313 fp.close()
1313 fp.close()
1314 try:
1314 try:
1315 output = self.run0('commit',
1315 output = self.run0('commit',
1316 username=util.shortuser(commit.author),
1316 username=util.shortuser(commit.author),
1317 file=messagefile,
1317 file=messagefile,
1318 encoding='utf-8')
1318 encoding='utf-8')
1319 try:
1319 try:
1320 rev = self.commit_re.search(output).group(1)
1320 rev = self.commit_re.search(output).group(1)
1321 except AttributeError:
1321 except AttributeError:
1322 if parents and not files:
1322 if parents and not files:
1323 return parents[0]
1323 return parents[0]
1324 self.ui.warn(_('unexpected svn output:\n'))
1324 self.ui.warn(_('unexpected svn output:\n'))
1325 self.ui.warn(output)
1325 self.ui.warn(output)
1326 raise error.Abort(_('unable to cope with svn output'))
1326 raise error.Abort(_('unable to cope with svn output'))
1327 if commit.rev:
1327 if commit.rev:
1328 self.run('propset', 'hg:convert-rev', commit.rev,
1328 self.run('propset', 'hg:convert-rev', commit.rev,
1329 revprop=True, revision=rev)
1329 revprop=True, revision=rev)
1330 if commit.branch and commit.branch != 'default':
1330 if commit.branch and commit.branch != 'default':
1331 self.run('propset', 'hg:convert-branch', commit.branch,
1331 self.run('propset', 'hg:convert-branch', commit.branch,
1332 revprop=True, revision=rev)
1332 revprop=True, revision=rev)
1333 for parent in parents:
1333 for parent in parents:
1334 self.addchild(parent, rev)
1334 self.addchild(parent, rev)
1335 return self.revid(rev)
1335 return self.revid(rev)
1336 finally:
1336 finally:
1337 os.unlink(messagefile)
1337 os.unlink(messagefile)
1338
1338
1339 def puttags(self, tags):
1339 def puttags(self, tags):
1340 self.ui.warn(_('writing Subversion tags is not yet implemented\n'))
1340 self.ui.warn(_('writing Subversion tags is not yet implemented\n'))
1341 return None, None
1341 return None, None
1342
1342
1343 def hascommitfrommap(self, rev):
1343 def hascommitfrommap(self, rev):
1344 # We trust that revisions referenced in a map still is present
1344 # We trust that revisions referenced in a map still is present
1345 # TODO: implement something better if necessary and feasible
1345 # TODO: implement something better if necessary and feasible
1346 return True
1346 return True
1347
1347
1348 def hascommitforsplicemap(self, rev):
1348 def hascommitforsplicemap(self, rev):
1349 # This is not correct as one can convert to an existing subversion
1349 # This is not correct as one can convert to an existing subversion
1350 # repository and childmap would not list all revisions. Too bad.
1350 # repository and childmap would not list all revisions. Too bad.
1351 if rev in self.childmap:
1351 if rev in self.childmap:
1352 return True
1352 return True
1353 raise error.Abort(_('splice map revision %s not found in subversion '
1353 raise error.Abort(_('splice map revision %s not found in subversion '
1354 'child map (revision lookups are not implemented)')
1354 'child map (revision lookups are not implemented)')
1355 % rev)
1355 % rev)
@@ -1,144 +1,144
1 #require svn svn-bindings
1 #require svn svn-bindings
2
2
3 $ cat >> $HGRCPATH <<EOF
3 $ cat >> $HGRCPATH <<EOF
4 > [extensions]
4 > [extensions]
5 > convert =
5 > convert =
6 > EOF
6 > EOF
7
7
8 $ svnadmin create svn-repo
8 $ svnadmin create svn-repo
9 $ svnadmin load -q svn-repo < "$TESTDIR/svn/encoding.svndump"
9 $ svnadmin load -q svn-repo < "$TESTDIR/svn/encoding.svndump"
10
10
11 Convert while testing all possible outputs
11 Convert while testing all possible outputs
12
12
13 $ hg --debug convert svn-repo A-hg --config progress.debug=1
13 $ hg --debug convert svn-repo A-hg --config progress.debug=1
14 initializing destination A-hg repository
14 initializing destination A-hg repository
15 reparent to file://*/svn-repo (glob)
15 reparent to file://*/svn-repo (glob)
16 run hg sink pre-conversion action
16 run hg sink pre-conversion action
17 scanning source...
17 scanning source...
18 found trunk at 'trunk'
18 found trunk at 'trunk'
19 found tags at 'tags'
19 found tags at 'tags'
20 found branches at 'branches'
20 found branches at 'branches'
21 found branch branch\xc3\xa9 at 5 (esc)
21 found branch branch\xc3\xa9 at 5 (esc)
22 found branch branch\xc3\xa9e at 6 (esc)
22 found branch branch\xc3\xa9e at 6 (esc)
23 scanning: 1/4 revisions (25.00%)
23 scanning: 1/4 revisions (25.00%)
24 reparent to file://*/svn-repo/trunk (glob)
24 reparent to file://*/svn-repo/trunk (glob)
25 fetching revision log for "/trunk" from 4 to 0
25 fetching revision log for "/trunk" from 4 to 0
26 parsing revision 4 (2 changes)
26 parsing revision 4 (2 changes)
27 parsing revision 3 (4 changes)
27 parsing revision 3 (4 changes)
28 parsing revision 2 (3 changes)
28 parsing revision 2 (3 changes)
29 parsing revision 1 (3 changes)
29 parsing revision 1 (3 changes)
30 no copyfrom path, don't know what to do.
30 no copyfrom path, don't know what to do.
31 '/branches' is not under '/trunk', ignoring
31 '/branches' is not under '/trunk', ignoring
32 '/tags' is not under '/trunk', ignoring
32 '/tags' is not under '/trunk', ignoring
33 scanning: 2/4 revisions (50.00%)
33 scanning: 2/4 revisions (50.00%)
34 reparent to file://*/svn-repo/branches/branch%C3%A9 (glob)
34 reparent to file://*/svn-repo/branches/branch%C3%A9 (glob)
35 fetching revision log for "/branches/branch\xc3\xa9" from 5 to 0 (esc)
35 fetching revision log for "/branches/branch\xc3\xa9" from 5 to 0 (esc)
36 parsing revision 5 (1 changes)
36 parsing revision 5 (1 changes)
37 reparent to file://*/svn-repo (glob)
37 reparent to file://*/svn-repo (glob)
38 reparent to file://*/svn-repo/branches/branch%C3%A9 (glob)
38 reparent to file://*/svn-repo/branches/branch%C3%A9 (glob)
39 found parent of branch /branches/branch\xc3\xa9 at 4: /trunk (esc)
39 found parent of branch /branches/branch\xc3\xa9 at 4: /trunk (esc)
40 scanning: 3/4 revisions (75.00%)
40 scanning: 3/4 revisions (75.00%)
41 reparent to file://*/svn-repo/branches/branch%C3%A9e (glob)
41 reparent to file://*/svn-repo/branches/branch%C3%A9e (glob)
42 fetching revision log for "/branches/branch\xc3\xa9e" from 6 to 0 (esc)
42 fetching revision log for "/branches/branch\xc3\xa9e" from 6 to 0 (esc)
43 parsing revision 6 (1 changes)
43 parsing revision 6 (1 changes)
44 reparent to file://*/svn-repo (glob)
44 reparent to file://*/svn-repo (glob)
45 reparent to file://*/svn-repo/branches/branch%C3%A9e (glob)
45 reparent to file://*/svn-repo/branches/branch%C3%A9e (glob)
46 found parent of branch /branches/branch\xc3\xa9e at 5: /branches/branch\xc3\xa9 (esc)
46 found parent of branch /branches/branch\xc3\xa9e at 5: /branches/branch\xc3\xa9 (esc)
47 scanning: 4/4 revisions (100.00%)
47 scanning: 4/4 revisions (100.00%)
48 scanning: 5/4 revisions (125.00%)
48 scanning: 5/4 revisions (125.00%)
49 scanning: 6/4 revisions (150.00%)
49 scanning: 6/4 revisions (150.00%)
50 sorting...
50 sorting...
51 converting...
51 converting...
52 5 init projA
52 5 init projA
53 source: svn:afeb9c47-92ff-4c0c-9f72-e1f6eb8ac9af/trunk@1
53 source: svn:afeb9c47-92ff-4c0c-9f72-e1f6eb8ac9af/trunk@1
54 converting: 0/6 revisions (0.00%)
54 converting: 0/6 revisions (0.00%)
55 committing changelog
55 committing changelog
56 4 hello
56 4 hello
57 source: svn:afeb9c47-92ff-4c0c-9f72-e1f6eb8ac9af/trunk@2
57 source: svn:afeb9c47-92ff-4c0c-9f72-e1f6eb8ac9af/trunk@2
58 converting: 1/6 revisions (16.67%)
58 converting: 1/6 revisions (16.67%)
59 reparent to file://*/svn-repo/trunk (glob)
59 reparent to file://*/svn-repo/trunk (glob)
60 scanning paths: /trunk/\xc3\xa0 0/3 (0.00%) (esc)
60 scanning paths: /trunk/\xc3\xa0 0/3 paths (0.00%) (esc)
61 scanning paths: /trunk/\xc3\xa0/e\xcc\x81 1/3 (33.33%) (esc)
61 scanning paths: /trunk/\xc3\xa0/e\xcc\x81 1/3 paths (33.33%) (esc)
62 scanning paths: /trunk/\xc3\xa9 2/3 (66.67%) (esc)
62 scanning paths: /trunk/\xc3\xa9 2/3 paths (66.67%) (esc)
63 committing files:
63 committing files:
64 \xc3\xa0/e\xcc\x81 (esc)
64 \xc3\xa0/e\xcc\x81 (esc)
65 getting files: \xc3\xa0/e\xcc\x81 1/2 files (50.00%) (esc)
65 getting files: \xc3\xa0/e\xcc\x81 1/2 files (50.00%) (esc)
66 \xc3\xa9 (esc)
66 \xc3\xa9 (esc)
67 getting files: \xc3\xa9 2/2 files (100.00%) (esc)
67 getting files: \xc3\xa9 2/2 files (100.00%) (esc)
68 committing manifest
68 committing manifest
69 committing changelog
69 committing changelog
70 3 copy files
70 3 copy files
71 source: svn:afeb9c47-92ff-4c0c-9f72-e1f6eb8ac9af/trunk@3
71 source: svn:afeb9c47-92ff-4c0c-9f72-e1f6eb8ac9af/trunk@3
72 converting: 2/6 revisions (33.33%)
72 converting: 2/6 revisions (33.33%)
73 scanning paths: /trunk/\xc3\xa0 0/4 (0.00%) (esc)
73 scanning paths: /trunk/\xc3\xa0 0/4 paths (0.00%) (esc)
74 gone from -1
74 gone from -1
75 reparent to file://*/svn-repo (glob)
75 reparent to file://*/svn-repo (glob)
76 reparent to file://*/svn-repo/trunk (glob)
76 reparent to file://*/svn-repo/trunk (glob)
77 scanning paths: /trunk/\xc3\xa8 1/4 (25.00%) (esc)
77 scanning paths: /trunk/\xc3\xa8 1/4 paths (25.00%) (esc)
78 copied to \xc3\xa8 from \xc3\xa9@2 (esc)
78 copied to \xc3\xa8 from \xc3\xa9@2 (esc)
79 scanning paths: /trunk/\xc3\xa9 2/4 (50.00%) (esc)
79 scanning paths: /trunk/\xc3\xa9 2/4 paths (50.00%) (esc)
80 gone from -1
80 gone from -1
81 reparent to file://*/svn-repo (glob)
81 reparent to file://*/svn-repo (glob)
82 reparent to file://*/svn-repo/trunk (glob)
82 reparent to file://*/svn-repo/trunk (glob)
83 scanning paths: /trunk/\xc3\xb9 3/4 (75.00%) (esc)
83 scanning paths: /trunk/\xc3\xb9 3/4 paths (75.00%) (esc)
84 mark /trunk/\xc3\xb9 came from \xc3\xa0:2 (esc)
84 mark /trunk/\xc3\xb9 came from \xc3\xa0:2 (esc)
85 getting files: \xc3\xa0/e\xcc\x81 1/4 files (25.00%) (esc)
85 getting files: \xc3\xa0/e\xcc\x81 1/4 files (25.00%) (esc)
86 getting files: \xc3\xa9 2/4 files (50.00%) (esc)
86 getting files: \xc3\xa9 2/4 files (50.00%) (esc)
87 committing files:
87 committing files:
88 \xc3\xa8 (esc)
88 \xc3\xa8 (esc)
89 getting files: \xc3\xa8 3/4 files (75.00%) (esc)
89 getting files: \xc3\xa8 3/4 files (75.00%) (esc)
90 \xc3\xa8: copy \xc3\xa9:6b67ccefd5ce6de77e7ead4f5292843a0255329f (esc)
90 \xc3\xa8: copy \xc3\xa9:6b67ccefd5ce6de77e7ead4f5292843a0255329f (esc)
91 \xc3\xb9/e\xcc\x81 (esc)
91 \xc3\xb9/e\xcc\x81 (esc)
92 getting files: \xc3\xb9/e\xcc\x81 4/4 files (100.00%) (esc)
92 getting files: \xc3\xb9/e\xcc\x81 4/4 files (100.00%) (esc)
93 \xc3\xb9/e\xcc\x81: copy \xc3\xa0/e\xcc\x81:a9092a3d84a37b9993b5c73576f6de29b7ea50f6 (esc)
93 \xc3\xb9/e\xcc\x81: copy \xc3\xa0/e\xcc\x81:a9092a3d84a37b9993b5c73576f6de29b7ea50f6 (esc)
94 committing manifest
94 committing manifest
95 committing changelog
95 committing changelog
96 2 remove files
96 2 remove files
97 source: svn:afeb9c47-92ff-4c0c-9f72-e1f6eb8ac9af/trunk@4
97 source: svn:afeb9c47-92ff-4c0c-9f72-e1f6eb8ac9af/trunk@4
98 converting: 3/6 revisions (50.00%)
98 converting: 3/6 revisions (50.00%)
99 scanning paths: /trunk/\xc3\xa8 0/2 (0.00%) (esc)
99 scanning paths: /trunk/\xc3\xa8 0/2 paths (0.00%) (esc)
100 gone from -1
100 gone from -1
101 reparent to file://*/svn-repo (glob)
101 reparent to file://*/svn-repo (glob)
102 reparent to file://*/svn-repo/trunk (glob)
102 reparent to file://*/svn-repo/trunk (glob)
103 scanning paths: /trunk/\xc3\xb9 1/2 (50.00%) (esc)
103 scanning paths: /trunk/\xc3\xb9 1/2 paths (50.00%) (esc)
104 gone from -1
104 gone from -1
105 reparent to file://*/svn-repo (glob)
105 reparent to file://*/svn-repo (glob)
106 reparent to file://*/svn-repo/trunk (glob)
106 reparent to file://*/svn-repo/trunk (glob)
107 getting files: \xc3\xa8 1/2 files (50.00%) (esc)
107 getting files: \xc3\xa8 1/2 files (50.00%) (esc)
108 getting files: \xc3\xb9/e\xcc\x81 2/2 files (100.00%) (esc)
108 getting files: \xc3\xb9/e\xcc\x81 2/2 files (100.00%) (esc)
109 committing files:
109 committing files:
110 committing manifest
110 committing manifest
111 committing changelog
111 committing changelog
112 1 branch to branch?
112 1 branch to branch?
113 source: svn:afeb9c47-92ff-4c0c-9f72-e1f6eb8ac9af/branches/branch?@5
113 source: svn:afeb9c47-92ff-4c0c-9f72-e1f6eb8ac9af/branches/branch?@5
114 converting: 4/6 revisions (66.67%)
114 converting: 4/6 revisions (66.67%)
115 reparent to file://*/svn-repo/branches/branch%C3%A9 (glob)
115 reparent to file://*/svn-repo/branches/branch%C3%A9 (glob)
116 scanning paths: /branches/branch\xc3\xa9 0/1 (0.00%) (esc)
116 scanning paths: /branches/branch\xc3\xa9 0/1 paths (0.00%) (esc)
117 committing changelog
117 committing changelog
118 0 branch to branch?e
118 0 branch to branch?e
119 source: svn:afeb9c47-92ff-4c0c-9f72-e1f6eb8ac9af/branches/branch?e@6
119 source: svn:afeb9c47-92ff-4c0c-9f72-e1f6eb8ac9af/branches/branch?e@6
120 converting: 5/6 revisions (83.33%)
120 converting: 5/6 revisions (83.33%)
121 reparent to file://*/svn-repo/branches/branch%C3%A9e (glob)
121 reparent to file://*/svn-repo/branches/branch%C3%A9e (glob)
122 scanning paths: /branches/branch\xc3\xa9e 0/1 (0.00%) (esc)
122 scanning paths: /branches/branch\xc3\xa9e 0/1 paths (0.00%) (esc)
123 committing changelog
123 committing changelog
124 reparent to file://*/svn-repo (glob)
124 reparent to file://*/svn-repo (glob)
125 reparent to file://*/svn-repo/branches/branch%C3%A9e (glob)
125 reparent to file://*/svn-repo/branches/branch%C3%A9e (glob)
126 reparent to file://*/svn-repo (glob)
126 reparent to file://*/svn-repo (glob)
127 reparent to file://*/svn-repo/branches/branch%C3%A9e (glob)
127 reparent to file://*/svn-repo/branches/branch%C3%A9e (glob)
128 updating tags
128 updating tags
129 committing files:
129 committing files:
130 .hgtags
130 .hgtags
131 committing manifest
131 committing manifest
132 committing changelog
132 committing changelog
133 run hg sink post-conversion action
133 run hg sink post-conversion action
134 $ cd A-hg
134 $ cd A-hg
135 $ hg up
135 $ hg up
136 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
136 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
137
137
138 Check tags are in UTF-8
138 Check tags are in UTF-8
139
139
140 $ cat .hgtags
140 $ cat .hgtags
141 e94e4422020e715add80525e8f0f46c9968689f1 branch\xc3\xa9e (esc)
141 e94e4422020e715add80525e8f0f46c9968689f1 branch\xc3\xa9e (esc)
142 f7e66f98380ed1e53a797c5c7a7a2616a7ab377d branch\xc3\xa9 (esc)
142 f7e66f98380ed1e53a797c5c7a7a2616a7ab377d branch\xc3\xa9 (esc)
143
143
144 $ cd ..
144 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now