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