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