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