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