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