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