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