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