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