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