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