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