##// END OF EJS Templates
convert/svn: report path discovery progress...
Patrick Mezard -
r11137:94afe7bc default
parent child Browse files
Show More
@@ -1,1157 +1,1160 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 getchanges(self, rev):
374 def getchanges(self, rev):
375 if self._changescache and self._changescache[0] == rev:
375 if self._changescache and self._changescache[0] == rev:
376 return self._changescache[1]
376 return self._changescache[1]
377 self._changescache = None
377 self._changescache = None
378 (paths, parents) = self.paths[rev]
378 (paths, parents) = self.paths[rev]
379 if parents:
379 if parents:
380 files, self.removed, copies = self.expandpaths(rev, paths, parents)
380 files, self.removed, copies = self.expandpaths(rev, paths, parents)
381 else:
381 else:
382 # Perform a full checkout on roots
382 # Perform a full checkout on roots
383 uuid, module, revnum = self.revsplit(rev)
383 uuid, module, revnum = self.revsplit(rev)
384 entries = svn.client.ls(self.baseurl + urllib.quote(module),
384 entries = svn.client.ls(self.baseurl + urllib.quote(module),
385 optrev(revnum), True, self.ctx)
385 optrev(revnum), True, self.ctx)
386 files = [n for n, e in entries.iteritems()
386 files = [n for n, e in entries.iteritems()
387 if e.kind == svn.core.svn_node_file]
387 if e.kind == svn.core.svn_node_file]
388 copies = {}
388 copies = {}
389 self.removed = set()
389 self.removed = set()
390
390
391 files.sort()
391 files.sort()
392 files = zip(files, [rev] * len(files))
392 files = zip(files, [rev] * len(files))
393
393
394 # caller caches the result, so free it here to release memory
394 # caller caches the result, so free it here to release memory
395 del self.paths[rev]
395 del self.paths[rev]
396 return (files, copies)
396 return (files, copies)
397
397
398 def getchangedfiles(self, rev, i):
398 def getchangedfiles(self, rev, i):
399 changes = self.getchanges(rev)
399 changes = self.getchanges(rev)
400 self._changescache = (rev, changes)
400 self._changescache = (rev, changes)
401 return [f[0] for f in changes[0]]
401 return [f[0] for f in changes[0]]
402
402
403 def getcommit(self, rev):
403 def getcommit(self, rev):
404 if rev not in self.commits:
404 if rev not in self.commits:
405 uuid, module, revnum = self.revsplit(rev)
405 uuid, module, revnum = self.revsplit(rev)
406 self.module = module
406 self.module = module
407 self.reparent(module)
407 self.reparent(module)
408 # We assume that:
408 # We assume that:
409 # - requests for revisions after "stop" come from the
409 # - requests for revisions after "stop" come from the
410 # revision graph backward traversal. Cache all of them
410 # revision graph backward traversal. Cache all of them
411 # down to stop, they will be used eventually.
411 # down to stop, they will be used eventually.
412 # - requests for revisions before "stop" come to get
412 # - requests for revisions before "stop" come to get
413 # isolated branches parents. Just fetch what is needed.
413 # isolated branches parents. Just fetch what is needed.
414 stop = self.lastrevs.get(module, 0)
414 stop = self.lastrevs.get(module, 0)
415 if revnum < stop:
415 if revnum < stop:
416 stop = revnum + 1
416 stop = revnum + 1
417 self._fetch_revisions(revnum, stop)
417 self._fetch_revisions(revnum, stop)
418 commit = self.commits[rev]
418 commit = self.commits[rev]
419 # caller caches the result, so free it here to release memory
419 # caller caches the result, so free it here to release memory
420 del self.commits[rev]
420 del self.commits[rev]
421 return commit
421 return commit
422
422
423 def gettags(self):
423 def gettags(self):
424 tags = {}
424 tags = {}
425 if self.tags is None:
425 if self.tags is None:
426 return tags
426 return tags
427
427
428 # svn tags are just a convention, project branches left in a
428 # svn tags are just a convention, project branches left in a
429 # 'tags' directory. There is no other relationship than
429 # 'tags' directory. There is no other relationship than
430 # ancestry, which is expensive to discover and makes them hard
430 # ancestry, which is expensive to discover and makes them hard
431 # to update incrementally. Worse, past revisions may be
431 # to update incrementally. Worse, past revisions may be
432 # referenced by tags far away in the future, requiring a deep
432 # referenced by tags far away in the future, requiring a deep
433 # history traversal on every calculation. Current code
433 # history traversal on every calculation. Current code
434 # performs a single backward traversal, tracking moves within
434 # performs a single backward traversal, tracking moves within
435 # the tags directory (tag renaming) and recording a new tag
435 # the tags directory (tag renaming) and recording a new tag
436 # everytime a project is copied from outside the tags
436 # everytime a project is copied from outside the tags
437 # directory. It also lists deleted tags, this behaviour may
437 # directory. It also lists deleted tags, this behaviour may
438 # change in the future.
438 # change in the future.
439 pendings = []
439 pendings = []
440 tagspath = self.tags
440 tagspath = self.tags
441 start = svn.ra.get_latest_revnum(self.ra)
441 start = svn.ra.get_latest_revnum(self.ra)
442 try:
442 try:
443 for entry in self._getlog([self.tags], start, self.startrev):
443 for entry in self._getlog([self.tags], start, self.startrev):
444 origpaths, revnum, author, date, message = entry
444 origpaths, revnum, author, date, message = entry
445 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
445 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
446 in origpaths.iteritems() if e.copyfrom_path]
446 in origpaths.iteritems() if e.copyfrom_path]
447 # Apply moves/copies from more specific to general
447 # Apply moves/copies from more specific to general
448 copies.sort(reverse=True)
448 copies.sort(reverse=True)
449
449
450 srctagspath = tagspath
450 srctagspath = tagspath
451 if copies and copies[-1][2] == tagspath:
451 if copies and copies[-1][2] == tagspath:
452 # Track tags directory moves
452 # Track tags directory moves
453 srctagspath = copies.pop()[0]
453 srctagspath = copies.pop()[0]
454
454
455 for source, sourcerev, dest in copies:
455 for source, sourcerev, dest in copies:
456 if not dest.startswith(tagspath + '/'):
456 if not dest.startswith(tagspath + '/'):
457 continue
457 continue
458 for tag in pendings:
458 for tag in pendings:
459 if tag[0].startswith(dest):
459 if tag[0].startswith(dest):
460 tagpath = source + tag[0][len(dest):]
460 tagpath = source + tag[0][len(dest):]
461 tag[:2] = [tagpath, sourcerev]
461 tag[:2] = [tagpath, sourcerev]
462 break
462 break
463 else:
463 else:
464 pendings.append([source, sourcerev, dest])
464 pendings.append([source, sourcerev, dest])
465
465
466 # Filter out tags with children coming from different
466 # Filter out tags with children coming from different
467 # parts of the repository like:
467 # parts of the repository like:
468 # /tags/tag.1 (from /trunk:10)
468 # /tags/tag.1 (from /trunk:10)
469 # /tags/tag.1/foo (from /branches/foo:12)
469 # /tags/tag.1/foo (from /branches/foo:12)
470 # Here/tags/tag.1 discarded as well as its children.
470 # Here/tags/tag.1 discarded as well as its children.
471 # It happens with tools like cvs2svn. Such tags cannot
471 # It happens with tools like cvs2svn. Such tags cannot
472 # be represented in mercurial.
472 # be represented in mercurial.
473 addeds = dict((p, e.copyfrom_path) for p, e
473 addeds = dict((p, e.copyfrom_path) for p, e
474 in origpaths.iteritems()
474 in origpaths.iteritems()
475 if e.action == 'A' and e.copyfrom_path)
475 if e.action == 'A' and e.copyfrom_path)
476 badroots = set()
476 badroots = set()
477 for destroot in addeds:
477 for destroot in addeds:
478 for source, sourcerev, dest in pendings:
478 for source, sourcerev, dest in pendings:
479 if (not dest.startswith(destroot + '/')
479 if (not dest.startswith(destroot + '/')
480 or source.startswith(addeds[destroot] + '/')):
480 or source.startswith(addeds[destroot] + '/')):
481 continue
481 continue
482 badroots.add(destroot)
482 badroots.add(destroot)
483 break
483 break
484
484
485 for badroot in badroots:
485 for badroot in badroots:
486 pendings = [p for p in pendings if p[2] != badroot
486 pendings = [p for p in pendings if p[2] != badroot
487 and not p[2].startswith(badroot + '/')]
487 and not p[2].startswith(badroot + '/')]
488
488
489 # Tell tag renamings from tag creations
489 # Tell tag renamings from tag creations
490 remainings = []
490 remainings = []
491 for source, sourcerev, dest in pendings:
491 for source, sourcerev, dest in pendings:
492 tagname = dest.split('/')[-1]
492 tagname = dest.split('/')[-1]
493 if source.startswith(srctagspath):
493 if source.startswith(srctagspath):
494 remainings.append([source, sourcerev, tagname])
494 remainings.append([source, sourcerev, tagname])
495 continue
495 continue
496 if tagname in tags:
496 if tagname in tags:
497 # Keep the latest tag value
497 # Keep the latest tag value
498 continue
498 continue
499 # From revision may be fake, get one with changes
499 # From revision may be fake, get one with changes
500 try:
500 try:
501 tagid = self.latest(source, sourcerev)
501 tagid = self.latest(source, sourcerev)
502 if tagid and tagname not in tags:
502 if tagid and tagname not in tags:
503 tags[tagname] = tagid
503 tags[tagname] = tagid
504 except SvnPathNotFound:
504 except SvnPathNotFound:
505 # It happens when we are following directories
505 # It happens when we are following directories
506 # we assumed were copied with their parents
506 # we assumed were copied with their parents
507 # but were really created in the tag
507 # but were really created in the tag
508 # directory.
508 # directory.
509 pass
509 pass
510 pendings = remainings
510 pendings = remainings
511 tagspath = srctagspath
511 tagspath = srctagspath
512
512
513 except SubversionException:
513 except SubversionException:
514 self.ui.note(_('no tags found at revision %d\n') % start)
514 self.ui.note(_('no tags found at revision %d\n') % start)
515 return tags
515 return tags
516
516
517 def converted(self, rev, destrev):
517 def converted(self, rev, destrev):
518 if not self.wc:
518 if not self.wc:
519 return
519 return
520 if self.convertfp is None:
520 if self.convertfp is None:
521 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
521 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
522 'a')
522 'a')
523 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
523 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
524 self.convertfp.flush()
524 self.convertfp.flush()
525
525
526 def revid(self, revnum, module=None):
526 def revid(self, revnum, module=None):
527 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
527 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
528
528
529 def revnum(self, rev):
529 def revnum(self, rev):
530 return int(rev.split('@')[-1])
530 return int(rev.split('@')[-1])
531
531
532 def revsplit(self, rev):
532 def revsplit(self, rev):
533 url, revnum = rev.rsplit('@', 1)
533 url, revnum = rev.rsplit('@', 1)
534 revnum = int(revnum)
534 revnum = int(revnum)
535 parts = url.split('/', 1)
535 parts = url.split('/', 1)
536 uuid = parts.pop(0)[4:]
536 uuid = parts.pop(0)[4:]
537 mod = ''
537 mod = ''
538 if parts:
538 if parts:
539 mod = '/' + parts[0]
539 mod = '/' + parts[0]
540 return uuid, mod, revnum
540 return uuid, mod, revnum
541
541
542 def latest(self, path, stop=0):
542 def latest(self, path, stop=0):
543 """Find the latest revid affecting path, up to stop. It may return
543 """Find the latest revid affecting path, up to stop. It may return
544 a revision in a different module, since a branch may be moved without
544 a revision in a different module, since a branch may be moved without
545 a change being reported. Return None if computed module does not
545 a change being reported. Return None if computed module does not
546 belong to rootmodule subtree.
546 belong to rootmodule subtree.
547 """
547 """
548 if not path.startswith(self.rootmodule):
548 if not path.startswith(self.rootmodule):
549 # Requests on foreign branches may be forbidden at server level
549 # Requests on foreign branches may be forbidden at server level
550 self.ui.debug('ignoring foreign branch %r\n' % path)
550 self.ui.debug('ignoring foreign branch %r\n' % path)
551 return None
551 return None
552
552
553 if not stop:
553 if not stop:
554 stop = svn.ra.get_latest_revnum(self.ra)
554 stop = svn.ra.get_latest_revnum(self.ra)
555 try:
555 try:
556 prevmodule = self.reparent('')
556 prevmodule = self.reparent('')
557 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
557 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
558 self.reparent(prevmodule)
558 self.reparent(prevmodule)
559 except SubversionException:
559 except SubversionException:
560 dirent = None
560 dirent = None
561 if not dirent:
561 if not dirent:
562 raise SvnPathNotFound(_('%s not found up to revision %d')
562 raise SvnPathNotFound(_('%s not found up to revision %d')
563 % (path, stop))
563 % (path, stop))
564
564
565 # stat() gives us the previous revision on this line of
565 # stat() gives us the previous revision on this line of
566 # development, but it might be in *another module*. Fetch the
566 # development, but it might be in *another module*. Fetch the
567 # log and detect renames down to the latest revision.
567 # log and detect renames down to the latest revision.
568 stream = self._getlog([path], stop, dirent.created_rev)
568 stream = self._getlog([path], stop, dirent.created_rev)
569 try:
569 try:
570 for entry in stream:
570 for entry in stream:
571 paths, revnum, author, date, message = entry
571 paths, revnum, author, date, message = entry
572 if revnum <= dirent.created_rev:
572 if revnum <= dirent.created_rev:
573 break
573 break
574
574
575 for p in paths:
575 for p in paths:
576 if not path.startswith(p) or not paths[p].copyfrom_path:
576 if not path.startswith(p) or not paths[p].copyfrom_path:
577 continue
577 continue
578 newpath = paths[p].copyfrom_path + path[len(p):]
578 newpath = paths[p].copyfrom_path + path[len(p):]
579 self.ui.debug("branch renamed from %s to %s at %d\n" %
579 self.ui.debug("branch renamed from %s to %s at %d\n" %
580 (path, newpath, revnum))
580 (path, newpath, revnum))
581 path = newpath
581 path = newpath
582 break
582 break
583 finally:
583 finally:
584 stream.close()
584 stream.close()
585
585
586 if not path.startswith(self.rootmodule):
586 if not path.startswith(self.rootmodule):
587 self.ui.debug('ignoring foreign branch %r\n' % path)
587 self.ui.debug('ignoring foreign branch %r\n' % path)
588 return None
588 return None
589 return self.revid(dirent.created_rev, path)
589 return self.revid(dirent.created_rev, path)
590
590
591 def reparent(self, module):
591 def reparent(self, module):
592 """Reparent the svn transport and return the previous parent."""
592 """Reparent the svn transport and return the previous parent."""
593 if self.prevmodule == module:
593 if self.prevmodule == module:
594 return module
594 return module
595 svnurl = self.baseurl + urllib.quote(module)
595 svnurl = self.baseurl + urllib.quote(module)
596 prevmodule = self.prevmodule
596 prevmodule = self.prevmodule
597 if prevmodule is None:
597 if prevmodule is None:
598 prevmodule = ''
598 prevmodule = ''
599 self.ui.debug("reparent to %s\n" % svnurl)
599 self.ui.debug("reparent to %s\n" % svnurl)
600 svn.ra.reparent(self.ra, svnurl)
600 svn.ra.reparent(self.ra, svnurl)
601 self.prevmodule = module
601 self.prevmodule = module
602 return prevmodule
602 return prevmodule
603
603
604 def expandpaths(self, rev, paths, parents):
604 def expandpaths(self, rev, paths, parents):
605 changed, removed = set(), set()
605 changed, removed = set(), set()
606 copies = {}
606 copies = {}
607
607
608 new_module, revnum = self.revsplit(rev)[1:]
608 new_module, revnum = self.revsplit(rev)[1:]
609 if new_module != self.module:
609 if new_module != self.module:
610 self.module = new_module
610 self.module = new_module
611 self.reparent(self.module)
611 self.reparent(self.module)
612
612
613 for path, ent in paths:
613 for i, (path, ent) in enumerate(paths):
614 self.ui.progress(_('scanning paths'), i, item=path,
615 total=len(paths))
614 entrypath = self.getrelpath(path)
616 entrypath = self.getrelpath(path)
615
617
616 kind = self._checkpath(entrypath, revnum)
618 kind = self._checkpath(entrypath, revnum)
617 if kind == svn.core.svn_node_file:
619 if kind == svn.core.svn_node_file:
618 changed.add(self.recode(entrypath))
620 changed.add(self.recode(entrypath))
619 if not ent.copyfrom_path or not parents:
621 if not ent.copyfrom_path or not parents:
620 continue
622 continue
621 # Copy sources not in parent revisions cannot be
623 # Copy sources not in parent revisions cannot be
622 # represented, ignore their origin for now
624 # represented, ignore their origin for now
623 pmodule, prevnum = self.revsplit(parents[0])[1:]
625 pmodule, prevnum = self.revsplit(parents[0])[1:]
624 if ent.copyfrom_rev < prevnum:
626 if ent.copyfrom_rev < prevnum:
625 continue
627 continue
626 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
628 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
627 if not copyfrom_path:
629 if not copyfrom_path:
628 continue
630 continue
629 self.ui.debug("copied to %s from %s@%s\n" %
631 self.ui.debug("copied to %s from %s@%s\n" %
630 (entrypath, copyfrom_path, ent.copyfrom_rev))
632 (entrypath, copyfrom_path, ent.copyfrom_rev))
631 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
633 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
632 elif kind == 0: # gone, but had better be a deleted *file*
634 elif kind == 0: # gone, but had better be a deleted *file*
633 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
635 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
634 pmodule, prevnum = self.revsplit(parents[0])[1:]
636 pmodule, prevnum = self.revsplit(parents[0])[1:]
635 parentpath = pmodule + "/" + entrypath
637 parentpath = pmodule + "/" + entrypath
636 fromkind = self._checkpath(entrypath, prevnum, pmodule)
638 fromkind = self._checkpath(entrypath, prevnum, pmodule)
637
639
638 if fromkind == svn.core.svn_node_file:
640 if fromkind == svn.core.svn_node_file:
639 removed.add(self.recode(entrypath))
641 removed.add(self.recode(entrypath))
640 elif fromkind == svn.core.svn_node_dir:
642 elif fromkind == svn.core.svn_node_dir:
641 oroot = parentpath.strip('/')
643 oroot = parentpath.strip('/')
642 nroot = path.strip('/')
644 nroot = path.strip('/')
643 children = self._iterfiles(oroot, prevnum)
645 children = self._iterfiles(oroot, prevnum)
644 for childpath in children:
646 for childpath in children:
645 childpath = childpath.replace(oroot, nroot)
647 childpath = childpath.replace(oroot, nroot)
646 childpath = self.getrelpath("/" + childpath, pmodule)
648 childpath = self.getrelpath("/" + childpath, pmodule)
647 if childpath:
649 if childpath:
648 removed.add(self.recode(childpath))
650 removed.add(self.recode(childpath))
649 else:
651 else:
650 self.ui.debug('unknown path in revision %d: %s\n' % \
652 self.ui.debug('unknown path in revision %d: %s\n' % \
651 (revnum, path))
653 (revnum, path))
652 elif kind == svn.core.svn_node_dir:
654 elif kind == svn.core.svn_node_dir:
653 if ent.action == 'M':
655 if ent.action == 'M':
654 # If the directory just had a prop change,
656 # If the directory just had a prop change,
655 # then we shouldn't need to look for its children.
657 # then we shouldn't need to look for its children.
656 continue
658 continue
657 elif ent.action == 'R' and parents:
659 elif ent.action == 'R' and parents:
658 # If a directory is replacing a file, mark the previous
660 # If a directory is replacing a file, mark the previous
659 # file as deleted
661 # file as deleted
660 pmodule, prevnum = self.revsplit(parents[0])[1:]
662 pmodule, prevnum = self.revsplit(parents[0])[1:]
661 pkind = self._checkpath(entrypath, prevnum, pmodule)
663 pkind = self._checkpath(entrypath, prevnum, pmodule)
662 if pkind == svn.core.svn_node_file:
664 if pkind == svn.core.svn_node_file:
663 removed.add(self.recode(entrypath))
665 removed.add(self.recode(entrypath))
664
666
665 for childpath in self._iterfiles(path, revnum):
667 for childpath in self._iterfiles(path, revnum):
666 childpath = self.getrelpath("/" + childpath)
668 childpath = self.getrelpath("/" + childpath)
667 if childpath:
669 if childpath:
668 changed.add(self.recode(childpath))
670 changed.add(self.recode(childpath))
669
671
670 # Handle directory copies
672 # Handle directory copies
671 if not ent.copyfrom_path or not parents:
673 if not ent.copyfrom_path or not parents:
672 continue
674 continue
673 # Copy sources not in parent revisions cannot be
675 # Copy sources not in parent revisions cannot be
674 # represented, ignore their origin for now
676 # represented, ignore their origin for now
675 pmodule, prevnum = self.revsplit(parents[0])[1:]
677 pmodule, prevnum = self.revsplit(parents[0])[1:]
676 if ent.copyfrom_rev < prevnum:
678 if ent.copyfrom_rev < prevnum:
677 continue
679 continue
678 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
680 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
679 if not copyfrompath:
681 if not copyfrompath:
680 continue
682 continue
681 self.ui.debug("mark %s came from %s:%d\n"
683 self.ui.debug("mark %s came from %s:%d\n"
682 % (path, copyfrompath, ent.copyfrom_rev))
684 % (path, copyfrompath, ent.copyfrom_rev))
683 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
685 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
684 for childpath in children:
686 for childpath in children:
685 childpath = self.getrelpath("/" + childpath, pmodule)
687 childpath = self.getrelpath("/" + childpath, pmodule)
686 if not childpath:
688 if not childpath:
687 continue
689 continue
688 copytopath = path + childpath[len(copyfrompath):]
690 copytopath = path + childpath[len(copyfrompath):]
689 copytopath = self.getrelpath(copytopath)
691 copytopath = self.getrelpath(copytopath)
690 copies[self.recode(copytopath)] = self.recode(childpath)
692 copies[self.recode(copytopath)] = self.recode(childpath)
691
693
694 self.ui.progress(_('scanning paths'), None)
692 changed.update(removed)
695 changed.update(removed)
693 return (list(changed), removed, copies)
696 return (list(changed), removed, copies)
694
697
695 def _fetch_revisions(self, from_revnum, to_revnum):
698 def _fetch_revisions(self, from_revnum, to_revnum):
696 if from_revnum < to_revnum:
699 if from_revnum < to_revnum:
697 from_revnum, to_revnum = to_revnum, from_revnum
700 from_revnum, to_revnum = to_revnum, from_revnum
698
701
699 self.child_cset = None
702 self.child_cset = None
700
703
701 def parselogentry(orig_paths, revnum, author, date, message):
704 def parselogentry(orig_paths, revnum, author, date, message):
702 """Return the parsed commit object or None, and True if
705 """Return the parsed commit object or None, and True if
703 the revision is a branch root.
706 the revision is a branch root.
704 """
707 """
705 self.ui.debug("parsing revision %d (%d changes)\n" %
708 self.ui.debug("parsing revision %d (%d changes)\n" %
706 (revnum, len(orig_paths)))
709 (revnum, len(orig_paths)))
707
710
708 branched = False
711 branched = False
709 rev = self.revid(revnum)
712 rev = self.revid(revnum)
710 # branch log might return entries for a parent we already have
713 # branch log might return entries for a parent we already have
711
714
712 if rev in self.commits or revnum < to_revnum:
715 if rev in self.commits or revnum < to_revnum:
713 return None, branched
716 return None, branched
714
717
715 parents = []
718 parents = []
716 # check whether this revision is the start of a branch or part
719 # check whether this revision is the start of a branch or part
717 # of a branch renaming
720 # of a branch renaming
718 orig_paths = sorted(orig_paths.iteritems())
721 orig_paths = sorted(orig_paths.iteritems())
719 root_paths = [(p, e) for p, e in orig_paths
722 root_paths = [(p, e) for p, e in orig_paths
720 if self.module.startswith(p)]
723 if self.module.startswith(p)]
721 if root_paths:
724 if root_paths:
722 path, ent = root_paths[-1]
725 path, ent = root_paths[-1]
723 if ent.copyfrom_path:
726 if ent.copyfrom_path:
724 branched = True
727 branched = True
725 newpath = ent.copyfrom_path + self.module[len(path):]
728 newpath = ent.copyfrom_path + self.module[len(path):]
726 # ent.copyfrom_rev may not be the actual last revision
729 # ent.copyfrom_rev may not be the actual last revision
727 previd = self.latest(newpath, ent.copyfrom_rev)
730 previd = self.latest(newpath, ent.copyfrom_rev)
728 if previd is not None:
731 if previd is not None:
729 prevmodule, prevnum = self.revsplit(previd)[1:]
732 prevmodule, prevnum = self.revsplit(previd)[1:]
730 if prevnum >= self.startrev:
733 if prevnum >= self.startrev:
731 parents = [previd]
734 parents = [previd]
732 self.ui.note(
735 self.ui.note(
733 _('found parent of branch %s at %d: %s\n') %
736 _('found parent of branch %s at %d: %s\n') %
734 (self.module, prevnum, prevmodule))
737 (self.module, prevnum, prevmodule))
735 else:
738 else:
736 self.ui.debug("no copyfrom path, don't know what to do.\n")
739 self.ui.debug("no copyfrom path, don't know what to do.\n")
737
740
738 paths = []
741 paths = []
739 # filter out unrelated paths
742 # filter out unrelated paths
740 for path, ent in orig_paths:
743 for path, ent in orig_paths:
741 if self.getrelpath(path) is None:
744 if self.getrelpath(path) is None:
742 continue
745 continue
743 paths.append((path, ent))
746 paths.append((path, ent))
744
747
745 # Example SVN datetime. Includes microseconds.
748 # Example SVN datetime. Includes microseconds.
746 # ISO-8601 conformant
749 # ISO-8601 conformant
747 # '2007-01-04T17:35:00.902377Z'
750 # '2007-01-04T17:35:00.902377Z'
748 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
751 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
749
752
750 log = message and self.recode(message) or ''
753 log = message and self.recode(message) or ''
751 author = author and self.recode(author) or ''
754 author = author and self.recode(author) or ''
752 try:
755 try:
753 branch = self.module.split("/")[-1]
756 branch = self.module.split("/")[-1]
754 if branch == 'trunk':
757 if branch == 'trunk':
755 branch = ''
758 branch = ''
756 except IndexError:
759 except IndexError:
757 branch = None
760 branch = None
758
761
759 cset = commit(author=author,
762 cset = commit(author=author,
760 date=util.datestr(date),
763 date=util.datestr(date),
761 desc=log,
764 desc=log,
762 parents=parents,
765 parents=parents,
763 branch=branch,
766 branch=branch,
764 rev=rev)
767 rev=rev)
765
768
766 self.commits[rev] = cset
769 self.commits[rev] = cset
767 # The parents list is *shared* among self.paths and the
770 # The parents list is *shared* among self.paths and the
768 # commit object. Both will be updated below.
771 # commit object. Both will be updated below.
769 self.paths[rev] = (paths, cset.parents)
772 self.paths[rev] = (paths, cset.parents)
770 if self.child_cset and not self.child_cset.parents:
773 if self.child_cset and not self.child_cset.parents:
771 self.child_cset.parents[:] = [rev]
774 self.child_cset.parents[:] = [rev]
772 self.child_cset = cset
775 self.child_cset = cset
773 return cset, branched
776 return cset, branched
774
777
775 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
778 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
776 (self.module, from_revnum, to_revnum))
779 (self.module, from_revnum, to_revnum))
777
780
778 try:
781 try:
779 firstcset = None
782 firstcset = None
780 lastonbranch = False
783 lastonbranch = False
781 stream = self._getlog([self.module], from_revnum, to_revnum)
784 stream = self._getlog([self.module], from_revnum, to_revnum)
782 try:
785 try:
783 for entry in stream:
786 for entry in stream:
784 paths, revnum, author, date, message = entry
787 paths, revnum, author, date, message = entry
785 if revnum < self.startrev:
788 if revnum < self.startrev:
786 lastonbranch = True
789 lastonbranch = True
787 break
790 break
788 if not paths:
791 if not paths:
789 self.ui.debug('revision %d has no entries\n' % revnum)
792 self.ui.debug('revision %d has no entries\n' % revnum)
790 # If we ever leave the loop on an empty
793 # If we ever leave the loop on an empty
791 # revision, do not try to get a parent branch
794 # revision, do not try to get a parent branch
792 lastonbranch = lastonbranch or revnum == 0
795 lastonbranch = lastonbranch or revnum == 0
793 continue
796 continue
794 cset, lastonbranch = parselogentry(paths, revnum, author,
797 cset, lastonbranch = parselogentry(paths, revnum, author,
795 date, message)
798 date, message)
796 if cset:
799 if cset:
797 firstcset = cset
800 firstcset = cset
798 if lastonbranch:
801 if lastonbranch:
799 break
802 break
800 finally:
803 finally:
801 stream.close()
804 stream.close()
802
805
803 if not lastonbranch and firstcset and not firstcset.parents:
806 if not lastonbranch and firstcset and not firstcset.parents:
804 # The first revision of the sequence (the last fetched one)
807 # The first revision of the sequence (the last fetched one)
805 # has invalid parents if not a branch root. Find the parent
808 # has invalid parents if not a branch root. Find the parent
806 # revision now, if any.
809 # revision now, if any.
807 try:
810 try:
808 firstrevnum = self.revnum(firstcset.rev)
811 firstrevnum = self.revnum(firstcset.rev)
809 if firstrevnum > 1:
812 if firstrevnum > 1:
810 latest = self.latest(self.module, firstrevnum - 1)
813 latest = self.latest(self.module, firstrevnum - 1)
811 if latest:
814 if latest:
812 firstcset.parents.append(latest)
815 firstcset.parents.append(latest)
813 except SvnPathNotFound:
816 except SvnPathNotFound:
814 pass
817 pass
815 except SubversionException, (inst, num):
818 except SubversionException, (inst, num):
816 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
819 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
817 raise util.Abort(_('svn: branch has no revision %s') % to_revnum)
820 raise util.Abort(_('svn: branch has no revision %s') % to_revnum)
818 raise
821 raise
819
822
820 def getfile(self, file, rev):
823 def getfile(self, file, rev):
821 # TODO: ra.get_file transmits the whole file instead of diffs.
824 # TODO: ra.get_file transmits the whole file instead of diffs.
822 if file in self.removed:
825 if file in self.removed:
823 raise IOError()
826 raise IOError()
824 mode = ''
827 mode = ''
825 try:
828 try:
826 new_module, revnum = self.revsplit(rev)[1:]
829 new_module, revnum = self.revsplit(rev)[1:]
827 if self.module != new_module:
830 if self.module != new_module:
828 self.module = new_module
831 self.module = new_module
829 self.reparent(self.module)
832 self.reparent(self.module)
830 io = StringIO()
833 io = StringIO()
831 info = svn.ra.get_file(self.ra, file, revnum, io)
834 info = svn.ra.get_file(self.ra, file, revnum, io)
832 data = io.getvalue()
835 data = io.getvalue()
833 # ra.get_files() seems to keep a reference on the input buffer
836 # ra.get_files() seems to keep a reference on the input buffer
834 # preventing collection. Release it explicitely.
837 # preventing collection. Release it explicitely.
835 io.close()
838 io.close()
836 if isinstance(info, list):
839 if isinstance(info, list):
837 info = info[-1]
840 info = info[-1]
838 mode = ("svn:executable" in info) and 'x' or ''
841 mode = ("svn:executable" in info) and 'x' or ''
839 mode = ("svn:special" in info) and 'l' or mode
842 mode = ("svn:special" in info) and 'l' or mode
840 except SubversionException, e:
843 except SubversionException, e:
841 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
844 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
842 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
845 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
843 if e.apr_err in notfound: # File not found
846 if e.apr_err in notfound: # File not found
844 raise IOError()
847 raise IOError()
845 raise
848 raise
846 if mode == 'l':
849 if mode == 'l':
847 link_prefix = "link "
850 link_prefix = "link "
848 if data.startswith(link_prefix):
851 if data.startswith(link_prefix):
849 data = data[len(link_prefix):]
852 data = data[len(link_prefix):]
850 return data, mode
853 return data, mode
851
854
852 def _iterfiles(self, path, revnum):
855 def _iterfiles(self, path, revnum):
853 """Enumerate all files in path at revnum, recursively."""
856 """Enumerate all files in path at revnum, recursively."""
854 path = path.strip('/')
857 path = path.strip('/')
855 pool = Pool()
858 pool = Pool()
856 rpath = '/'.join([self.baseurl, urllib.quote(path)]).strip('/')
859 rpath = '/'.join([self.baseurl, urllib.quote(path)]).strip('/')
857 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
860 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
858 return ((path + '/' + p) for p, e in entries.iteritems()
861 return ((path + '/' + p) for p, e in entries.iteritems()
859 if e.kind == svn.core.svn_node_file)
862 if e.kind == svn.core.svn_node_file)
860
863
861 def getrelpath(self, path, module=None):
864 def getrelpath(self, path, module=None):
862 if module is None:
865 if module is None:
863 module = self.module
866 module = self.module
864 # Given the repository url of this wc, say
867 # Given the repository url of this wc, say
865 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
868 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
866 # extract the "entry" portion (a relative path) from what
869 # extract the "entry" portion (a relative path) from what
867 # svn log --xml says, ie
870 # svn log --xml says, ie
868 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
871 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
869 # that is to say "tests/PloneTestCase.py"
872 # that is to say "tests/PloneTestCase.py"
870 if path.startswith(module):
873 if path.startswith(module):
871 relative = path.rstrip('/')[len(module):]
874 relative = path.rstrip('/')[len(module):]
872 if relative.startswith('/'):
875 if relative.startswith('/'):
873 return relative[1:]
876 return relative[1:]
874 elif relative == '':
877 elif relative == '':
875 return relative
878 return relative
876
879
877 # The path is outside our tracked tree...
880 # The path is outside our tracked tree...
878 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
881 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
879 return None
882 return None
880
883
881 def _checkpath(self, path, revnum, module=None):
884 def _checkpath(self, path, revnum, module=None):
882 if module is not None:
885 if module is not None:
883 prevmodule = self.reparent('')
886 prevmodule = self.reparent('')
884 path = module + '/' + path
887 path = module + '/' + path
885 try:
888 try:
886 # ra.check_path does not like leading slashes very much, it leads
889 # ra.check_path does not like leading slashes very much, it leads
887 # to PROPFIND subversion errors
890 # to PROPFIND subversion errors
888 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
891 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
889 finally:
892 finally:
890 if module is not None:
893 if module is not None:
891 self.reparent(prevmodule)
894 self.reparent(prevmodule)
892
895
893 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
896 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
894 strict_node_history=False):
897 strict_node_history=False):
895 # Normalize path names, svn >= 1.5 only wants paths relative to
898 # Normalize path names, svn >= 1.5 only wants paths relative to
896 # supplied URL
899 # supplied URL
897 relpaths = []
900 relpaths = []
898 for p in paths:
901 for p in paths:
899 if not p.startswith('/'):
902 if not p.startswith('/'):
900 p = self.module + '/' + p
903 p = self.module + '/' + p
901 relpaths.append(p.strip('/'))
904 relpaths.append(p.strip('/'))
902 args = [self.baseurl, relpaths, start, end, limit, discover_changed_paths,
905 args = [self.baseurl, relpaths, start, end, limit, discover_changed_paths,
903 strict_node_history]
906 strict_node_history]
904 arg = encodeargs(args)
907 arg = encodeargs(args)
905 hgexe = util.hgexecutable()
908 hgexe = util.hgexecutable()
906 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
909 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
907 stdin, stdout = util.popen2(cmd)
910 stdin, stdout = util.popen2(cmd)
908 stdin.write(arg)
911 stdin.write(arg)
909 try:
912 try:
910 stdin.close()
913 stdin.close()
911 except IOError:
914 except IOError:
912 raise util.Abort(_('Mercurial failed to run itself, check'
915 raise util.Abort(_('Mercurial failed to run itself, check'
913 ' hg executable is in PATH'))
916 ' hg executable is in PATH'))
914 return logstream(stdout)
917 return logstream(stdout)
915
918
916 pre_revprop_change = '''#!/bin/sh
919 pre_revprop_change = '''#!/bin/sh
917
920
918 REPOS="$1"
921 REPOS="$1"
919 REV="$2"
922 REV="$2"
920 USER="$3"
923 USER="$3"
921 PROPNAME="$4"
924 PROPNAME="$4"
922 ACTION="$5"
925 ACTION="$5"
923
926
924 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
927 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
925 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
928 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
926 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
929 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
927
930
928 echo "Changing prohibited revision property" >&2
931 echo "Changing prohibited revision property" >&2
929 exit 1
932 exit 1
930 '''
933 '''
931
934
932 class svn_sink(converter_sink, commandline):
935 class svn_sink(converter_sink, commandline):
933 commit_re = re.compile(r'Committed revision (\d+).', re.M)
936 commit_re = re.compile(r'Committed revision (\d+).', re.M)
934
937
935 def prerun(self):
938 def prerun(self):
936 if self.wc:
939 if self.wc:
937 os.chdir(self.wc)
940 os.chdir(self.wc)
938
941
939 def postrun(self):
942 def postrun(self):
940 if self.wc:
943 if self.wc:
941 os.chdir(self.cwd)
944 os.chdir(self.cwd)
942
945
943 def join(self, name):
946 def join(self, name):
944 return os.path.join(self.wc, '.svn', name)
947 return os.path.join(self.wc, '.svn', name)
945
948
946 def revmapfile(self):
949 def revmapfile(self):
947 return self.join('hg-shamap')
950 return self.join('hg-shamap')
948
951
949 def authorfile(self):
952 def authorfile(self):
950 return self.join('hg-authormap')
953 return self.join('hg-authormap')
951
954
952 def __init__(self, ui, path):
955 def __init__(self, ui, path):
953 converter_sink.__init__(self, ui, path)
956 converter_sink.__init__(self, ui, path)
954 commandline.__init__(self, ui, 'svn')
957 commandline.__init__(self, ui, 'svn')
955 self.delete = []
958 self.delete = []
956 self.setexec = []
959 self.setexec = []
957 self.delexec = []
960 self.delexec = []
958 self.copies = []
961 self.copies = []
959 self.wc = None
962 self.wc = None
960 self.cwd = os.getcwd()
963 self.cwd = os.getcwd()
961
964
962 path = os.path.realpath(path)
965 path = os.path.realpath(path)
963
966
964 created = False
967 created = False
965 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
968 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
966 self.wc = path
969 self.wc = path
967 self.run0('update')
970 self.run0('update')
968 else:
971 else:
969 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
972 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
970
973
971 if os.path.isdir(os.path.dirname(path)):
974 if os.path.isdir(os.path.dirname(path)):
972 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
975 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
973 ui.status(_('initializing svn repository %r\n') %
976 ui.status(_('initializing svn repository %r\n') %
974 os.path.basename(path))
977 os.path.basename(path))
975 commandline(ui, 'svnadmin').run0('create', path)
978 commandline(ui, 'svnadmin').run0('create', path)
976 created = path
979 created = path
977 path = util.normpath(path)
980 path = util.normpath(path)
978 if not path.startswith('/'):
981 if not path.startswith('/'):
979 path = '/' + path
982 path = '/' + path
980 path = 'file://' + path
983 path = 'file://' + path
981
984
982 ui.status(_('initializing svn working copy %r\n')
985 ui.status(_('initializing svn working copy %r\n')
983 % os.path.basename(wcpath))
986 % os.path.basename(wcpath))
984 self.run0('checkout', path, wcpath)
987 self.run0('checkout', path, wcpath)
985
988
986 self.wc = wcpath
989 self.wc = wcpath
987 self.opener = util.opener(self.wc)
990 self.opener = util.opener(self.wc)
988 self.wopener = util.opener(self.wc)
991 self.wopener = util.opener(self.wc)
989 self.childmap = mapfile(ui, self.join('hg-childmap'))
992 self.childmap = mapfile(ui, self.join('hg-childmap'))
990 self.is_exec = util.checkexec(self.wc) and util.is_exec or None
993 self.is_exec = util.checkexec(self.wc) and util.is_exec or None
991
994
992 if created:
995 if created:
993 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
996 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
994 fp = open(hook, 'w')
997 fp = open(hook, 'w')
995 fp.write(pre_revprop_change)
998 fp.write(pre_revprop_change)
996 fp.close()
999 fp.close()
997 util.set_flags(hook, False, True)
1000 util.set_flags(hook, False, True)
998
1001
999 xport = transport.SvnRaTransport(url=geturl(path))
1002 xport = transport.SvnRaTransport(url=geturl(path))
1000 self.uuid = svn.ra.get_uuid(xport.ra)
1003 self.uuid = svn.ra.get_uuid(xport.ra)
1001
1004
1002 def wjoin(self, *names):
1005 def wjoin(self, *names):
1003 return os.path.join(self.wc, *names)
1006 return os.path.join(self.wc, *names)
1004
1007
1005 def putfile(self, filename, flags, data):
1008 def putfile(self, filename, flags, data):
1006 if 'l' in flags:
1009 if 'l' in flags:
1007 self.wopener.symlink(data, filename)
1010 self.wopener.symlink(data, filename)
1008 else:
1011 else:
1009 try:
1012 try:
1010 if os.path.islink(self.wjoin(filename)):
1013 if os.path.islink(self.wjoin(filename)):
1011 os.unlink(filename)
1014 os.unlink(filename)
1012 except OSError:
1015 except OSError:
1013 pass
1016 pass
1014 self.wopener(filename, 'w').write(data)
1017 self.wopener(filename, 'w').write(data)
1015
1018
1016 if self.is_exec:
1019 if self.is_exec:
1017 was_exec = self.is_exec(self.wjoin(filename))
1020 was_exec = self.is_exec(self.wjoin(filename))
1018 else:
1021 else:
1019 # On filesystems not supporting execute-bit, there is no way
1022 # On filesystems not supporting execute-bit, there is no way
1020 # to know if it is set but asking subversion. Setting it
1023 # to know if it is set but asking subversion. Setting it
1021 # systematically is just as expensive and much simpler.
1024 # systematically is just as expensive and much simpler.
1022 was_exec = 'x' not in flags
1025 was_exec = 'x' not in flags
1023
1026
1024 util.set_flags(self.wjoin(filename), False, 'x' in flags)
1027 util.set_flags(self.wjoin(filename), False, 'x' in flags)
1025 if was_exec:
1028 if was_exec:
1026 if 'x' not in flags:
1029 if 'x' not in flags:
1027 self.delexec.append(filename)
1030 self.delexec.append(filename)
1028 else:
1031 else:
1029 if 'x' in flags:
1032 if 'x' in flags:
1030 self.setexec.append(filename)
1033 self.setexec.append(filename)
1031
1034
1032 def _copyfile(self, source, dest):
1035 def _copyfile(self, source, dest):
1033 # SVN's copy command pukes if the destination file exists, but
1036 # SVN's copy command pukes if the destination file exists, but
1034 # our copyfile method expects to record a copy that has
1037 # our copyfile method expects to record a copy that has
1035 # already occurred. Cross the semantic gap.
1038 # already occurred. Cross the semantic gap.
1036 wdest = self.wjoin(dest)
1039 wdest = self.wjoin(dest)
1037 exists = os.path.exists(wdest)
1040 exists = os.path.exists(wdest)
1038 if exists:
1041 if exists:
1039 fd, tempname = tempfile.mkstemp(
1042 fd, tempname = tempfile.mkstemp(
1040 prefix='hg-copy-', dir=os.path.dirname(wdest))
1043 prefix='hg-copy-', dir=os.path.dirname(wdest))
1041 os.close(fd)
1044 os.close(fd)
1042 os.unlink(tempname)
1045 os.unlink(tempname)
1043 os.rename(wdest, tempname)
1046 os.rename(wdest, tempname)
1044 try:
1047 try:
1045 self.run0('copy', source, dest)
1048 self.run0('copy', source, dest)
1046 finally:
1049 finally:
1047 if exists:
1050 if exists:
1048 try:
1051 try:
1049 os.unlink(wdest)
1052 os.unlink(wdest)
1050 except OSError:
1053 except OSError:
1051 pass
1054 pass
1052 os.rename(tempname, wdest)
1055 os.rename(tempname, wdest)
1053
1056
1054 def dirs_of(self, files):
1057 def dirs_of(self, files):
1055 dirs = set()
1058 dirs = set()
1056 for f in files:
1059 for f in files:
1057 if os.path.isdir(self.wjoin(f)):
1060 if os.path.isdir(self.wjoin(f)):
1058 dirs.add(f)
1061 dirs.add(f)
1059 for i in strutil.rfindall(f, '/'):
1062 for i in strutil.rfindall(f, '/'):
1060 dirs.add(f[:i])
1063 dirs.add(f[:i])
1061 return dirs
1064 return dirs
1062
1065
1063 def add_dirs(self, files):
1066 def add_dirs(self, files):
1064 add_dirs = [d for d in sorted(self.dirs_of(files))
1067 add_dirs = [d for d in sorted(self.dirs_of(files))
1065 if not os.path.exists(self.wjoin(d, '.svn', 'entries'))]
1068 if not os.path.exists(self.wjoin(d, '.svn', 'entries'))]
1066 if add_dirs:
1069 if add_dirs:
1067 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1070 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1068 return add_dirs
1071 return add_dirs
1069
1072
1070 def add_files(self, files):
1073 def add_files(self, files):
1071 if files:
1074 if files:
1072 self.xargs(files, 'add', quiet=True)
1075 self.xargs(files, 'add', quiet=True)
1073 return files
1076 return files
1074
1077
1075 def tidy_dirs(self, names):
1078 def tidy_dirs(self, names):
1076 deleted = []
1079 deleted = []
1077 for d in sorted(self.dirs_of(names), reverse=True):
1080 for d in sorted(self.dirs_of(names), reverse=True):
1078 wd = self.wjoin(d)
1081 wd = self.wjoin(d)
1079 if os.listdir(wd) == '.svn':
1082 if os.listdir(wd) == '.svn':
1080 self.run0('delete', d)
1083 self.run0('delete', d)
1081 deleted.append(d)
1084 deleted.append(d)
1082 return deleted
1085 return deleted
1083
1086
1084 def addchild(self, parent, child):
1087 def addchild(self, parent, child):
1085 self.childmap[parent] = child
1088 self.childmap[parent] = child
1086
1089
1087 def revid(self, rev):
1090 def revid(self, rev):
1088 return u"svn:%s@%s" % (self.uuid, rev)
1091 return u"svn:%s@%s" % (self.uuid, rev)
1089
1092
1090 def putcommit(self, files, copies, parents, commit, source, revmap):
1093 def putcommit(self, files, copies, parents, commit, source, revmap):
1091 # Apply changes to working copy
1094 # Apply changes to working copy
1092 for f, v in files:
1095 for f, v in files:
1093 try:
1096 try:
1094 data, mode = source.getfile(f, v)
1097 data, mode = source.getfile(f, v)
1095 except IOError:
1098 except IOError:
1096 self.delete.append(f)
1099 self.delete.append(f)
1097 else:
1100 else:
1098 self.putfile(f, mode, data)
1101 self.putfile(f, mode, data)
1099 if f in copies:
1102 if f in copies:
1100 self.copies.append([copies[f], f])
1103 self.copies.append([copies[f], f])
1101 files = [f[0] for f in files]
1104 files = [f[0] for f in files]
1102
1105
1103 for parent in parents:
1106 for parent in parents:
1104 try:
1107 try:
1105 return self.revid(self.childmap[parent])
1108 return self.revid(self.childmap[parent])
1106 except KeyError:
1109 except KeyError:
1107 pass
1110 pass
1108 entries = set(self.delete)
1111 entries = set(self.delete)
1109 files = frozenset(files)
1112 files = frozenset(files)
1110 entries.update(self.add_dirs(files.difference(entries)))
1113 entries.update(self.add_dirs(files.difference(entries)))
1111 if self.copies:
1114 if self.copies:
1112 for s, d in self.copies:
1115 for s, d in self.copies:
1113 self._copyfile(s, d)
1116 self._copyfile(s, d)
1114 self.copies = []
1117 self.copies = []
1115 if self.delete:
1118 if self.delete:
1116 self.xargs(self.delete, 'delete')
1119 self.xargs(self.delete, 'delete')
1117 self.delete = []
1120 self.delete = []
1118 entries.update(self.add_files(files.difference(entries)))
1121 entries.update(self.add_files(files.difference(entries)))
1119 entries.update(self.tidy_dirs(entries))
1122 entries.update(self.tidy_dirs(entries))
1120 if self.delexec:
1123 if self.delexec:
1121 self.xargs(self.delexec, 'propdel', 'svn:executable')
1124 self.xargs(self.delexec, 'propdel', 'svn:executable')
1122 self.delexec = []
1125 self.delexec = []
1123 if self.setexec:
1126 if self.setexec:
1124 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1127 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1125 self.setexec = []
1128 self.setexec = []
1126
1129
1127 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1130 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1128 fp = os.fdopen(fd, 'w')
1131 fp = os.fdopen(fd, 'w')
1129 fp.write(commit.desc)
1132 fp.write(commit.desc)
1130 fp.close()
1133 fp.close()
1131 try:
1134 try:
1132 output = self.run0('commit',
1135 output = self.run0('commit',
1133 username=util.shortuser(commit.author),
1136 username=util.shortuser(commit.author),
1134 file=messagefile,
1137 file=messagefile,
1135 encoding='utf-8')
1138 encoding='utf-8')
1136 try:
1139 try:
1137 rev = self.commit_re.search(output).group(1)
1140 rev = self.commit_re.search(output).group(1)
1138 except AttributeError:
1141 except AttributeError:
1139 if not files:
1142 if not files:
1140 return parents[0]
1143 return parents[0]
1141 self.ui.warn(_('unexpected svn output:\n'))
1144 self.ui.warn(_('unexpected svn output:\n'))
1142 self.ui.warn(output)
1145 self.ui.warn(output)
1143 raise util.Abort(_('unable to cope with svn output'))
1146 raise util.Abort(_('unable to cope with svn output'))
1144 if commit.rev:
1147 if commit.rev:
1145 self.run('propset', 'hg:convert-rev', commit.rev,
1148 self.run('propset', 'hg:convert-rev', commit.rev,
1146 revprop=True, revision=rev)
1149 revprop=True, revision=rev)
1147 if commit.branch and commit.branch != 'default':
1150 if commit.branch and commit.branch != 'default':
1148 self.run('propset', 'hg:convert-branch', commit.branch,
1151 self.run('propset', 'hg:convert-branch', commit.branch,
1149 revprop=True, revision=rev)
1152 revprop=True, revision=rev)
1150 for parent in parents:
1153 for parent in parents:
1151 self.addchild(parent, rev)
1154 self.addchild(parent, rev)
1152 return self.revid(rev)
1155 return self.revid(rev)
1153 finally:
1156 finally:
1154 os.unlink(messagefile)
1157 os.unlink(messagefile)
1155
1158
1156 def puttags(self, tags):
1159 def puttags(self, tags):
1157 self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n'))
1160 self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n'))
@@ -1,116 +1,123 b''
1 % convert trunk and branches
1 % convert trunk and branches
2 initializing destination A-hg repository
2 initializing destination A-hg repository
3 scanning source...
3 scanning source...
4 sorting...
4 sorting...
5 converting...
5 converting...
6 13 createtrunk
6 13 createtrunk
7 12 moved1
7 12 moved1
8 11 moved1
8 11 moved1
9 10 moved2
9 10 moved2
10 9 changeb and rm d2
10 9 changeb and rm d2
11 8 changeb and rm d2
11 8 changeb and rm d2
12 7 moved1again
12 7 moved1again
13 6 moved1again
13 6 moved1again
14 5 copyfilefrompast
14 5 copyfilefrompast
15 4 copydirfrompast
15 4 copydirfrompast
16 3 add d3
16 3 add d3
17 2 copy dir and remove subdir
17 2 copy dir and remove subdir
18 1 add d4old
18 1 add d4old
19 0 rename d4old into d4new
19 0 rename d4old into d4new
20 o 13 rename d4old into d4new files: d4new/g d4old/g
20 o 13 rename d4old into d4new files: d4new/g d4old/g
21 |
21 |
22 o 12 add d4old files: d4old/g
22 o 12 add d4old files: d4old/g
23 |
23 |
24 o 11 copy dir and remove subdir files: d3/d31/e d4/d31/e d4/f
24 o 11 copy dir and remove subdir files: d3/d31/e d4/d31/e d4/f
25 |
25 |
26 o 10 add d3 files: d3/d31/e d3/f
26 o 10 add d3 files: d3/d31/e d3/f
27 |
27 |
28 o 9 copydirfrompast files: d2/d
28 o 9 copydirfrompast files: d2/d
29 |
29 |
30 o 8 copyfilefrompast files: d
30 o 8 copyfilefrompast files: d
31 |
31 |
32 o 7 moved1again files: d1/b d1/c
32 o 7 moved1again files: d1/b d1/c
33 |
33 |
34 | o 6 moved1again files:
34 | o 6 moved1again files:
35 | |
35 | |
36 o | 5 changeb and rm d2 files: d1/b d2/d
36 o | 5 changeb and rm d2 files: d1/b d2/d
37 | |
37 | |
38 | o 4 changeb and rm d2 files: b
38 | o 4 changeb and rm d2 files: b
39 | |
39 | |
40 o | 3 moved2 files: d2/d
40 o | 3 moved2 files: d2/d
41 | |
41 | |
42 o | 2 moved1 files: d1/b d1/c
42 o | 2 moved1 files: d1/b d1/c
43 | |
43 | |
44 | o 1 moved1 files: b c
44 | o 1 moved1 files: b c
45 |
45 |
46 o 0 createtrunk files:
46 o 0 createtrunk files:
47
47
48 % check move copy records
48 % check move copy records
49 A d4new/g
49 A d4new/g
50 d4old/g
50 d4old/g
51 R d4old/g
51 R d4old/g
52 % check branches
52 % check branches
53 default 13:
53 default 13:
54 d1 6:
54 d1 6:
55 % convert files being replaced by directories
55 % convert files being replaced by directories
56 initializing destination hg-repo repository
56 initializing destination hg-repo repository
57 scanning source...
57 scanning source...
58 sorting...
58 sorting...
59 converting...
59 converting...
60 3 initial
60 3 initial
61 2 clobber symlink
61 2 clobber symlink
62 1 clobber1
62 1 clobber1
63 0 clobber2
63 0 clobber2
64 % manifest before
64 % manifest before
65 644 a
65 644 a
66 644 d/b
66 644 d/b
67 644 @ dlink
67 644 @ dlink
68 644 @ dlink2
68 644 @ dlink2
69 644 dlink3
69 644 dlink3
70 % manifest after clobber1
70 % manifest after clobber1
71 644 a/b
71 644 a/b
72 644 d/b
72 644 d/b
73 644 dlink/b
73 644 dlink/b
74 644 @ dlink2
74 644 @ dlink2
75 644 dlink3
75 644 dlink3
76 % manifest after clobber2
76 % manifest after clobber2
77 644 a/b
77 644 a/b
78 644 d/b
78 644 d/b
79 644 dlink/b
79 644 dlink/b
80 644 @ dlink2
80 644 @ dlink2
81 644 @ dlink3
81 644 @ dlink3
82 % try updating
82 % try updating
83 % test convert progress bar
83 % test convert progress bar
84
84
85 scanning [ <=> ] 1
85 scanning [ <=> ] 1
86 scanning [ <=> ] 2
86 scanning [ <=> ] 2
87 scanning [ <=> ] 3
87 scanning [ <=> ] 3
88 scanning [ <=> ] 4
88 scanning [ <=> ] 4
89
89
90 converting [ ] 0/4
90 converting [ ] 0/4
91 retrieving file [==========> ] 1/5
91 retrieving file [==========> ] 1/5
92 retrieving file [=====================> ] 2/5
92 retrieving file [=====================> ] 2/5
93 retrieving file [=================================> ] 3/5
93 retrieving file [=================================> ] 3/5
94 retrieving file [============================================> ] 4/5
94 retrieving file [============================================> ] 4/5
95 retrieving file [========================================================>] 5/5
95 retrieving file [========================================================>] 5/5
96
96
97 converting [==============> ] 1/4
97 converting [==============> ] 1/4
98 scanning paths [ ] 0/1
99
98 retrieving file [========================================================>] 1/1
100 retrieving file [========================================================>] 1/1
99
101
100 converting [==============================> ] 2/4
102 converting [==============================> ] 2/4
103 scanning paths [ ] 0/2
104 scanning paths [============================> ] 1/2
105
101 retrieving file [=============> ] 1/4
106 retrieving file [=============> ] 1/4
102 retrieving file [===========================> ] 2/4
107 retrieving file [===========================> ] 2/4
103 retrieving file [=========================================> ] 3/4
108 retrieving file [=========================================> ] 3/4
104 retrieving file [========================================================>] 4/4
109 retrieving file [========================================================>] 4/4
105
110
106 converting [=============================================> ] 3/4
111 converting [=============================================> ] 3/4
112 scanning paths [ ] 0/1
113
107 retrieving file [========================================================>] 1/1
114 retrieving file [========================================================>] 1/1
108
115
109 initializing destination hg-progress repository
116 initializing destination hg-progress repository
110 scanning source...
117 scanning source...
111 sorting...
118 sorting...
112 converting...
119 converting...
113 3 initial
120 3 initial
114 2 clobber symlink
121 2 clobber symlink
115 1 clobber1
122 1 clobber1
116 0 clobber2
123 0 clobber2
General Comments 0
You need to be logged in to leave comments. Login now