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