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