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