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