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