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