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