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