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