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