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