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