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