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