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