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