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