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