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