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