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