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