##// END OF EJS Templates
convert: handle past or foreign partial svn copies...
Patrick Mezard -
r6543:a6e2e60b default
parent child Browse files
Show More
@@ -1,1116 +1,1118 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 if ent.copyfrom_path:
555 if ent.copyfrom_path:
556 copyfrom_path = self.getrelpath(ent.copyfrom_path)
556 copyfrom_path = self.getrelpath(ent.copyfrom_path)
557 if copyfrom_path:
557 if copyfrom_path:
558 self.ui.debug("Copied to %s from %s@%s\n" %
558 self.ui.debug("Copied to %s from %s@%s\n" %
559 (entrypath, copyfrom_path,
559 (entrypath, copyfrom_path,
560 ent.copyfrom_rev))
560 ent.copyfrom_rev))
561 # It's probably important for hg that the source
561 # It's probably important for hg that the source
562 # exists in the revision's parent, not just the
562 # exists in the revision's parent, not just the
563 # ent.copyfrom_rev
563 # ent.copyfrom_rev
564 fromkind = svn.ra.check_path(self.ra, copyfrom_path, ent.copyfrom_rev)
564 fromkind = svn.ra.check_path(self.ra, copyfrom_path, ent.copyfrom_rev)
565 if fromkind != 0:
565 if fromkind != 0:
566 copies[self.recode(entry)] = self.recode(copyfrom_path)
566 copies[self.recode(entry)] = self.recode(copyfrom_path)
567 entries.append(self.recode(entry))
567 entries.append(self.recode(entry))
568 elif kind == 0: # gone, but had better be a deleted *file*
568 elif kind == 0: # gone, but had better be a deleted *file*
569 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
569 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
570
570
571 # if a branch is created but entries are removed in the same
571 # if a branch is created but entries are removed in the same
572 # changeset, get the right fromrev
572 # changeset, get the right fromrev
573 # parents cannot be empty here, you cannot remove things from
573 # parents cannot be empty here, you cannot remove things from
574 # a root revision.
574 # a root revision.
575 uuid, old_module, fromrev = self.revsplit(parents[0])
575 uuid, old_module, fromrev = self.revsplit(parents[0])
576
576
577 basepath = old_module + "/" + self.getrelpath(path)
577 basepath = old_module + "/" + self.getrelpath(path)
578 entrypath = basepath
578 entrypath = basepath
579
579
580 def lookup_parts(p):
580 def lookup_parts(p):
581 rc = None
581 rc = None
582 parts = p.split("/")
582 parts = p.split("/")
583 for i in range(len(parts)):
583 for i in range(len(parts)):
584 part = "/".join(parts[:i])
584 part = "/".join(parts[:i])
585 info = part, copyfrom.get(part, None)
585 info = part, copyfrom.get(part, None)
586 if info[1] is not None:
586 if info[1] is not None:
587 self.ui.debug("Found parent directory %s\n" % info[1])
587 self.ui.debug("Found parent directory %s\n" % info[1])
588 rc = info
588 rc = info
589 return rc
589 return rc
590
590
591 self.ui.debug("base, entry %s %s\n" % (basepath, entrypath))
591 self.ui.debug("base, entry %s %s\n" % (basepath, entrypath))
592
592
593 frompath, froment = lookup_parts(entrypath) or (None, revnum - 1)
593 frompath, froment = lookup_parts(entrypath) or (None, revnum - 1)
594
594
595 # need to remove fragment from lookup_parts and replace with copyfrom_path
595 # need to remove fragment from lookup_parts and replace with copyfrom_path
596 if frompath is not None:
596 if frompath is not None:
597 self.ui.debug("munge-o-matic\n")
597 self.ui.debug("munge-o-matic\n")
598 self.ui.debug(entrypath + '\n')
598 self.ui.debug(entrypath + '\n')
599 self.ui.debug(entrypath[len(frompath):] + '\n')
599 self.ui.debug(entrypath[len(frompath):] + '\n')
600 entrypath = froment.copyfrom_path + entrypath[len(frompath):]
600 entrypath = froment.copyfrom_path + entrypath[len(frompath):]
601 fromrev = froment.copyfrom_rev
601 fromrev = froment.copyfrom_rev
602 self.ui.debug("Info: %s %s %s %s\n" % (frompath, froment, ent, entrypath))
602 self.ui.debug("Info: %s %s %s %s\n" % (frompath, froment, ent, entrypath))
603
603
604 # We can avoid the reparent calls if the module has not changed
604 # We can avoid the reparent calls if the module has not changed
605 # but it probably does not worth the pain.
605 # but it probably does not worth the pain.
606 self.reparent('')
606 self.reparent('')
607 fromkind = svn.ra.check_path(self.ra, entrypath.strip('/'), fromrev)
607 fromkind = svn.ra.check_path(self.ra, entrypath.strip('/'), fromrev)
608 self.reparent(self.module)
608 self.reparent(self.module)
609
609
610 if fromkind == svn.core.svn_node_file: # a deleted file
610 if fromkind == svn.core.svn_node_file: # a deleted file
611 entries.append(self.recode(entry))
611 entries.append(self.recode(entry))
612 elif fromkind == svn.core.svn_node_dir:
612 elif fromkind == svn.core.svn_node_dir:
613 # print "Deleted/moved non-file:", revnum, path, ent
613 # print "Deleted/moved non-file:", revnum, path, ent
614 # children = self._find_children(path, revnum - 1)
614 # children = self._find_children(path, revnum - 1)
615 # print "find children %s@%d from %d action %s" % (path, revnum, ent.copyfrom_rev, ent.action)
615 # print "find children %s@%d from %d action %s" % (path, revnum, ent.copyfrom_rev, ent.action)
616 # Sometimes this is tricky. For example: in
616 # Sometimes this is tricky. For example: in
617 # The Subversion Repository revision 6940 a dir
617 # The Subversion Repository revision 6940 a dir
618 # was copied and one of its files was deleted
618 # was copied and one of its files was deleted
619 # from the new location in the same commit. This
619 # from the new location in the same commit. This
620 # code can't deal with that yet.
620 # code can't deal with that yet.
621 if ent.action == 'C':
621 if ent.action == 'C':
622 children = self._find_children(path, fromrev)
622 children = self._find_children(path, fromrev)
623 else:
623 else:
624 oroot = entrypath.strip('/')
624 oroot = entrypath.strip('/')
625 nroot = path.strip('/')
625 nroot = path.strip('/')
626 children = self._find_children(oroot, fromrev)
626 children = self._find_children(oroot, fromrev)
627 children = [s.replace(oroot,nroot) for s in children]
627 children = [s.replace(oroot,nroot) for s in children]
628 # Mark all [files, not directories] as deleted.
628 # Mark all [files, not directories] as deleted.
629 for child in children:
629 for child in children:
630 # Can we move a child directory and its
630 # Can we move a child directory and its
631 # parent in the same commit? (probably can). Could
631 # parent in the same commit? (probably can). Could
632 # cause problems if instead of revnum -1,
632 # cause problems if instead of revnum -1,
633 # we have to look in (copyfrom_path, revnum - 1)
633 # we have to look in (copyfrom_path, revnum - 1)
634 entrypath = self.getrelpath("/" + child, module=old_module)
634 entrypath = self.getrelpath("/" + child, module=old_module)
635 if entrypath:
635 if entrypath:
636 entry = self.recode(entrypath.decode(self.encoding))
636 entry = self.recode(entrypath.decode(self.encoding))
637 if entry in copies:
637 if entry in copies:
638 # deleted file within a copy
638 # deleted file within a copy
639 del copies[entry]
639 del copies[entry]
640 else:
640 else:
641 entries.append(entry)
641 entries.append(entry)
642 else:
642 else:
643 self.ui.debug('unknown path in revision %d: %s\n' % \
643 self.ui.debug('unknown path in revision %d: %s\n' % \
644 (revnum, path))
644 (revnum, path))
645 elif kind == svn.core.svn_node_dir:
645 elif kind == svn.core.svn_node_dir:
646 # Should probably synthesize normal file entries
646 # Should probably synthesize normal file entries
647 # and handle as above to clean up copy/rename handling.
647 # and handle as above to clean up copy/rename handling.
648
648
649 # If the directory just had a prop change,
649 # If the directory just had a prop change,
650 # then we shouldn't need to look for its children.
650 # then we shouldn't need to look for its children.
651 if ent.action == 'M':
651 if ent.action == 'M':
652 continue
652 continue
653
653
654 # Also this could create duplicate entries. Not sure
654 # Also this could create duplicate entries. Not sure
655 # whether this will matter. Maybe should make entries a set.
655 # whether this will matter. Maybe should make entries a set.
656 # print "Changed directory", revnum, path, ent.action, ent.copyfrom_path, ent.copyfrom_rev
656 # print "Changed directory", revnum, path, ent.action, ent.copyfrom_path, ent.copyfrom_rev
657 # This will fail if a directory was copied
657 # This will fail if a directory was copied
658 # from another branch and then some of its files
658 # from another branch and then some of its files
659 # were deleted in the same transaction.
659 # were deleted in the same transaction.
660 children = self._find_children(path, revnum)
660 children = self._find_children(path, revnum)
661 children.sort()
661 children.sort()
662 for child in children:
662 for child in children:
663 # Can we move a child directory and its
663 # Can we move a child directory and its
664 # parent in the same commit? (probably can). Could
664 # parent in the same commit? (probably can). Could
665 # cause problems if instead of revnum -1,
665 # cause problems if instead of revnum -1,
666 # we have to look in (copyfrom_path, revnum - 1)
666 # we have to look in (copyfrom_path, revnum - 1)
667 entrypath = self.getrelpath("/" + child)
667 entrypath = self.getrelpath("/" + child)
668 # print child, self.module, entrypath
668 # print child, self.module, entrypath
669 if entrypath:
669 if entrypath:
670 # Need to filter out directories here...
670 # Need to filter out directories here...
671 kind = svn.ra.check_path(self.ra, entrypath, revnum)
671 kind = svn.ra.check_path(self.ra, entrypath, revnum)
672 if kind != svn.core.svn_node_dir:
672 if kind != svn.core.svn_node_dir:
673 entries.append(self.recode(entrypath))
673 entries.append(self.recode(entrypath))
674
674
675 # Copies here (must copy all from source)
675 # Copies here (must copy all from source)
676 # Probably not a real problem for us if
676 # Probably not a real problem for us if
677 # source does not exist
677 # source does not exist
678 if not ent.copyfrom_path:
678 if not ent.copyfrom_path or not parents:
679 continue
679 continue
680 copyfrompath = self.getrelpath(ent.copyfrom_path.decode(self.encoding))
680 # Copy sources not in parent revisions cannot be represented,
681 # ignore their origin for now
682 pmodule, prevnum = self.revsplit(parents[0])[1:]
683 if ent.copyfrom_rev < prevnum:
684 continue
685 copyfrompath = ent.copyfrom_path.decode(self.encoding)
686 copyfrompath = self.getrelpath(copyfrompath, pmodule)
681 if not copyfrompath:
687 if not copyfrompath:
682 continue
688 continue
683 copyfrom[path] = ent
689 copyfrom[path] = ent
684 self.ui.debug("mark %s came from %s:%d\n"
690 self.ui.debug("mark %s came from %s:%d\n"
685 % (path, copyfrompath, ent.copyfrom_rev))
691 % (path, copyfrompath, ent.copyfrom_rev))
686
687 # Good, /probably/ a regular copy. Really should check
688 # to see whether the parent revision actually contains
689 # the directory in question.
690 children = self._find_children(ent.copyfrom_path, ent.copyfrom_rev)
692 children = self._find_children(ent.copyfrom_path, ent.copyfrom_rev)
691 children.sort()
693 children.sort()
692 for child in children:
694 for child in children:
693 entrypath = self.getrelpath("/" + child)
695 entrypath = self.getrelpath("/" + child, pmodule)
694 if not entrypath:
696 if not entrypath:
695 continue
697 continue
696 entry = entrypath.decode(self.encoding)
698 entry = entrypath.decode(self.encoding)
697 copytopath = path + entry[len(copyfrompath):]
699 copytopath = path + entry[len(copyfrompath):]
698 copytopath = self.getrelpath(copytopath)
700 copytopath = self.getrelpath(copytopath)
699 copies[self.recode(copytopath)] = self.recode(entry)
701 copies[self.recode(copytopath)] = self.recode(entry, pmodule)
700
702
701 return (util.unique(entries), copies)
703 return (util.unique(entries), copies)
702
704
703 def _fetch_revisions(self, from_revnum, to_revnum):
705 def _fetch_revisions(self, from_revnum, to_revnum):
704 if from_revnum < to_revnum:
706 if from_revnum < to_revnum:
705 from_revnum, to_revnum = to_revnum, from_revnum
707 from_revnum, to_revnum = to_revnum, from_revnum
706
708
707 self.child_cset = None
709 self.child_cset = None
708 def parselogentry(orig_paths, revnum, author, date, message):
710 def parselogentry(orig_paths, revnum, author, date, message):
709 """Return the parsed commit object or None, and True if
711 """Return the parsed commit object or None, and True if
710 the revision is a branch root.
712 the revision is a branch root.
711 """
713 """
712 self.ui.debug("parsing revision %d (%d changes)\n" %
714 self.ui.debug("parsing revision %d (%d changes)\n" %
713 (revnum, len(orig_paths)))
715 (revnum, len(orig_paths)))
714
716
715 branched = False
717 branched = False
716 rev = self.revid(revnum)
718 rev = self.revid(revnum)
717 # branch log might return entries for a parent we already have
719 # branch log might return entries for a parent we already have
718
720
719 if (rev in self.commits or revnum < to_revnum):
721 if (rev in self.commits or revnum < to_revnum):
720 return None, branched
722 return None, branched
721
723
722 parents = []
724 parents = []
723 # check whether this revision is the start of a branch or part
725 # check whether this revision is the start of a branch or part
724 # of a branch renaming
726 # of a branch renaming
725 orig_paths = orig_paths.items()
727 orig_paths = orig_paths.items()
726 orig_paths.sort()
728 orig_paths.sort()
727 root_paths = [(p,e) for p,e in orig_paths if self.module.startswith(p)]
729 root_paths = [(p,e) for p,e in orig_paths if self.module.startswith(p)]
728 if root_paths:
730 if root_paths:
729 path, ent = root_paths[-1]
731 path, ent = root_paths[-1]
730 if ent.copyfrom_path:
732 if ent.copyfrom_path:
731 branched = True
733 branched = True
732 newpath = ent.copyfrom_path + self.module[len(path):]
734 newpath = ent.copyfrom_path + self.module[len(path):]
733 # ent.copyfrom_rev may not be the actual last revision
735 # ent.copyfrom_rev may not be the actual last revision
734 previd = self.latest(newpath, ent.copyfrom_rev)
736 previd = self.latest(newpath, ent.copyfrom_rev)
735 if previd is not None:
737 if previd is not None:
736 prevmodule, prevnum = self.revsplit(previd)[1:]
738 prevmodule, prevnum = self.revsplit(previd)[1:]
737 if prevnum >= self.startrev:
739 if prevnum >= self.startrev:
738 parents = [previd]
740 parents = [previd]
739 self.ui.note('found parent of branch %s at %d: %s\n' %
741 self.ui.note('found parent of branch %s at %d: %s\n' %
740 (self.module, prevnum, prevmodule))
742 (self.module, prevnum, prevmodule))
741 else:
743 else:
742 self.ui.debug("No copyfrom path, don't know what to do.\n")
744 self.ui.debug("No copyfrom path, don't know what to do.\n")
743
745
744 paths = []
746 paths = []
745 # filter out unrelated paths
747 # filter out unrelated paths
746 for path, ent in orig_paths:
748 for path, ent in orig_paths:
747 if self.getrelpath(path) is None:
749 if self.getrelpath(path) is None:
748 continue
750 continue
749 paths.append((path, ent))
751 paths.append((path, ent))
750
752
751 # Example SVN datetime. Includes microseconds.
753 # Example SVN datetime. Includes microseconds.
752 # ISO-8601 conformant
754 # ISO-8601 conformant
753 # '2007-01-04T17:35:00.902377Z'
755 # '2007-01-04T17:35:00.902377Z'
754 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
756 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
755
757
756 log = message and self.recode(message) or ''
758 log = message and self.recode(message) or ''
757 author = author and self.recode(author) or ''
759 author = author and self.recode(author) or ''
758 try:
760 try:
759 branch = self.module.split("/")[-1]
761 branch = self.module.split("/")[-1]
760 if branch == 'trunk':
762 if branch == 'trunk':
761 branch = ''
763 branch = ''
762 except IndexError:
764 except IndexError:
763 branch = None
765 branch = None
764
766
765 cset = commit(author=author,
767 cset = commit(author=author,
766 date=util.datestr(date),
768 date=util.datestr(date),
767 desc=log,
769 desc=log,
768 parents=parents,
770 parents=parents,
769 branch=branch,
771 branch=branch,
770 rev=rev.encode('utf-8'))
772 rev=rev.encode('utf-8'))
771
773
772 self.commits[rev] = cset
774 self.commits[rev] = cset
773 # The parents list is *shared* among self.paths and the
775 # The parents list is *shared* among self.paths and the
774 # commit object. Both will be updated below.
776 # commit object. Both will be updated below.
775 self.paths[rev] = (paths, cset.parents)
777 self.paths[rev] = (paths, cset.parents)
776 if self.child_cset and not self.child_cset.parents:
778 if self.child_cset and not self.child_cset.parents:
777 self.child_cset.parents[:] = [rev]
779 self.child_cset.parents[:] = [rev]
778 self.child_cset = cset
780 self.child_cset = cset
779 return cset, branched
781 return cset, branched
780
782
781 self.ui.note('fetching revision log for "%s" from %d to %d\n' %
783 self.ui.note('fetching revision log for "%s" from %d to %d\n' %
782 (self.module, from_revnum, to_revnum))
784 (self.module, from_revnum, to_revnum))
783
785
784 try:
786 try:
785 firstcset = None
787 firstcset = None
786 lastonbranch = False
788 lastonbranch = False
787 stream = get_log(self.url, [self.module], from_revnum, to_revnum)
789 stream = get_log(self.url, [self.module], from_revnum, to_revnum)
788 try:
790 try:
789 for entry in stream:
791 for entry in stream:
790 paths, revnum, author, date, message = entry
792 paths, revnum, author, date, message = entry
791 if revnum < self.startrev:
793 if revnum < self.startrev:
792 lastonbranch = True
794 lastonbranch = True
793 break
795 break
794 if self.is_blacklisted(revnum):
796 if self.is_blacklisted(revnum):
795 self.ui.note('skipping blacklisted revision %d\n'
797 self.ui.note('skipping blacklisted revision %d\n'
796 % revnum)
798 % revnum)
797 continue
799 continue
798 if paths is None:
800 if paths is None:
799 self.ui.debug('revision %d has no entries\n' % revnum)
801 self.ui.debug('revision %d has no entries\n' % revnum)
800 continue
802 continue
801 cset, lastonbranch = parselogentry(paths, revnum, author,
803 cset, lastonbranch = parselogentry(paths, revnum, author,
802 date, message)
804 date, message)
803 if cset:
805 if cset:
804 firstcset = cset
806 firstcset = cset
805 if lastonbranch:
807 if lastonbranch:
806 break
808 break
807 finally:
809 finally:
808 stream.close()
810 stream.close()
809
811
810 if not lastonbranch and firstcset and not firstcset.parents:
812 if not lastonbranch and firstcset and not firstcset.parents:
811 # The first revision of the sequence (the last fetched one)
813 # The first revision of the sequence (the last fetched one)
812 # has invalid parents if not a branch root. Find the parent
814 # has invalid parents if not a branch root. Find the parent
813 # revision now, if any.
815 # revision now, if any.
814 try:
816 try:
815 firstrevnum = self.revnum(firstcset.rev)
817 firstrevnum = self.revnum(firstcset.rev)
816 if firstrevnum > 1:
818 if firstrevnum > 1:
817 latest = self.latest(self.module, firstrevnum - 1)
819 latest = self.latest(self.module, firstrevnum - 1)
818 if latest:
820 if latest:
819 firstcset.parents.append(latest)
821 firstcset.parents.append(latest)
820 except util.Abort:
822 except util.Abort:
821 pass
823 pass
822 except SubversionException, (inst, num):
824 except SubversionException, (inst, num):
823 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
825 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
824 raise util.Abort('svn: branch has no revision %s' % to_revnum)
826 raise util.Abort('svn: branch has no revision %s' % to_revnum)
825 raise
827 raise
826
828
827 def _getfile(self, file, rev):
829 def _getfile(self, file, rev):
828 io = StringIO()
830 io = StringIO()
829 # TODO: ra.get_file transmits the whole file instead of diffs.
831 # TODO: ra.get_file transmits the whole file instead of diffs.
830 mode = ''
832 mode = ''
831 try:
833 try:
832 new_module, revnum = self.revsplit(rev)[1:]
834 new_module, revnum = self.revsplit(rev)[1:]
833 if self.module != new_module:
835 if self.module != new_module:
834 self.module = new_module
836 self.module = new_module
835 self.reparent(self.module)
837 self.reparent(self.module)
836 info = svn.ra.get_file(self.ra, file, revnum, io)
838 info = svn.ra.get_file(self.ra, file, revnum, io)
837 if isinstance(info, list):
839 if isinstance(info, list):
838 info = info[-1]
840 info = info[-1]
839 mode = ("svn:executable" in info) and 'x' or ''
841 mode = ("svn:executable" in info) and 'x' or ''
840 mode = ("svn:special" in info) and 'l' or mode
842 mode = ("svn:special" in info) and 'l' or mode
841 except SubversionException, e:
843 except SubversionException, e:
842 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
844 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
843 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
845 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
844 if e.apr_err in notfound: # File not found
846 if e.apr_err in notfound: # File not found
845 raise IOError()
847 raise IOError()
846 raise
848 raise
847 data = io.getvalue()
849 data = io.getvalue()
848 if mode == 'l':
850 if mode == 'l':
849 link_prefix = "link "
851 link_prefix = "link "
850 if data.startswith(link_prefix):
852 if data.startswith(link_prefix):
851 data = data[len(link_prefix):]
853 data = data[len(link_prefix):]
852 return data, mode
854 return data, mode
853
855
854 def _find_children(self, path, revnum):
856 def _find_children(self, path, revnum):
855 path = path.strip('/')
857 path = path.strip('/')
856 pool = Pool()
858 pool = Pool()
857 rpath = '/'.join([self.base, path]).strip('/')
859 rpath = '/'.join([self.base, path]).strip('/')
858 return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
860 return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
859
861
860 def getrelpath(self, path, module=None):
862 def getrelpath(self, path, module=None):
861 if module is None:
863 if module is None:
862 module = self.module
864 module = self.module
863 # Given the repository url of this wc, say
865 # Given the repository url of this wc, say
864 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
866 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
865 # extract the "entry" portion (a relative path) from what
867 # extract the "entry" portion (a relative path) from what
866 # svn log --xml says, ie
868 # svn log --xml says, ie
867 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
869 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
868 # that is to say "tests/PloneTestCase.py"
870 # that is to say "tests/PloneTestCase.py"
869 if path.startswith(module):
871 if path.startswith(module):
870 relative = path.rstrip('/')[len(module):]
872 relative = path.rstrip('/')[len(module):]
871 if relative.startswith('/'):
873 if relative.startswith('/'):
872 return relative[1:]
874 return relative[1:]
873 elif relative == '':
875 elif relative == '':
874 return relative
876 return relative
875
877
876 # The path is outside our tracked tree...
878 # The path is outside our tracked tree...
877 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
879 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
878 return None
880 return None
879
881
880 pre_revprop_change = '''#!/bin/sh
882 pre_revprop_change = '''#!/bin/sh
881
883
882 REPOS="$1"
884 REPOS="$1"
883 REV="$2"
885 REV="$2"
884 USER="$3"
886 USER="$3"
885 PROPNAME="$4"
887 PROPNAME="$4"
886 ACTION="$5"
888 ACTION="$5"
887
889
888 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
890 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
889 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
891 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
890 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
892 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
891
893
892 echo "Changing prohibited revision property" >&2
894 echo "Changing prohibited revision property" >&2
893 exit 1
895 exit 1
894 '''
896 '''
895
897
896 class svn_sink(converter_sink, commandline):
898 class svn_sink(converter_sink, commandline):
897 commit_re = re.compile(r'Committed revision (\d+).', re.M)
899 commit_re = re.compile(r'Committed revision (\d+).', re.M)
898
900
899 def prerun(self):
901 def prerun(self):
900 if self.wc:
902 if self.wc:
901 os.chdir(self.wc)
903 os.chdir(self.wc)
902
904
903 def postrun(self):
905 def postrun(self):
904 if self.wc:
906 if self.wc:
905 os.chdir(self.cwd)
907 os.chdir(self.cwd)
906
908
907 def join(self, name):
909 def join(self, name):
908 return os.path.join(self.wc, '.svn', name)
910 return os.path.join(self.wc, '.svn', name)
909
911
910 def revmapfile(self):
912 def revmapfile(self):
911 return self.join('hg-shamap')
913 return self.join('hg-shamap')
912
914
913 def authorfile(self):
915 def authorfile(self):
914 return self.join('hg-authormap')
916 return self.join('hg-authormap')
915
917
916 def __init__(self, ui, path):
918 def __init__(self, ui, path):
917 converter_sink.__init__(self, ui, path)
919 converter_sink.__init__(self, ui, path)
918 commandline.__init__(self, ui, 'svn')
920 commandline.__init__(self, ui, 'svn')
919 self.delete = []
921 self.delete = []
920 self.setexec = []
922 self.setexec = []
921 self.delexec = []
923 self.delexec = []
922 self.copies = []
924 self.copies = []
923 self.wc = None
925 self.wc = None
924 self.cwd = os.getcwd()
926 self.cwd = os.getcwd()
925
927
926 path = os.path.realpath(path)
928 path = os.path.realpath(path)
927
929
928 created = False
930 created = False
929 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
931 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
930 self.wc = path
932 self.wc = path
931 self.run0('update')
933 self.run0('update')
932 else:
934 else:
933 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
935 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
934
936
935 if os.path.isdir(os.path.dirname(path)):
937 if os.path.isdir(os.path.dirname(path)):
936 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
938 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
937 ui.status(_('initializing svn repo %r\n') %
939 ui.status(_('initializing svn repo %r\n') %
938 os.path.basename(path))
940 os.path.basename(path))
939 commandline(ui, 'svnadmin').run0('create', path)
941 commandline(ui, 'svnadmin').run0('create', path)
940 created = path
942 created = path
941 path = util.normpath(path)
943 path = util.normpath(path)
942 if not path.startswith('/'):
944 if not path.startswith('/'):
943 path = '/' + path
945 path = '/' + path
944 path = 'file://' + path
946 path = 'file://' + path
945
947
946 ui.status(_('initializing svn wc %r\n') % os.path.basename(wcpath))
948 ui.status(_('initializing svn wc %r\n') % os.path.basename(wcpath))
947 self.run0('checkout', path, wcpath)
949 self.run0('checkout', path, wcpath)
948
950
949 self.wc = wcpath
951 self.wc = wcpath
950 self.opener = util.opener(self.wc)
952 self.opener = util.opener(self.wc)
951 self.wopener = util.opener(self.wc)
953 self.wopener = util.opener(self.wc)
952 self.childmap = mapfile(ui, self.join('hg-childmap'))
954 self.childmap = mapfile(ui, self.join('hg-childmap'))
953 self.is_exec = util.checkexec(self.wc) and util.is_exec or None
955 self.is_exec = util.checkexec(self.wc) and util.is_exec or None
954
956
955 if created:
957 if created:
956 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
958 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
957 fp = open(hook, 'w')
959 fp = open(hook, 'w')
958 fp.write(pre_revprop_change)
960 fp.write(pre_revprop_change)
959 fp.close()
961 fp.close()
960 util.set_flags(hook, "x")
962 util.set_flags(hook, "x")
961
963
962 xport = transport.SvnRaTransport(url=geturl(path))
964 xport = transport.SvnRaTransport(url=geturl(path))
963 self.uuid = svn.ra.get_uuid(xport.ra)
965 self.uuid = svn.ra.get_uuid(xport.ra)
964
966
965 def wjoin(self, *names):
967 def wjoin(self, *names):
966 return os.path.join(self.wc, *names)
968 return os.path.join(self.wc, *names)
967
969
968 def putfile(self, filename, flags, data):
970 def putfile(self, filename, flags, data):
969 if 'l' in flags:
971 if 'l' in flags:
970 self.wopener.symlink(data, filename)
972 self.wopener.symlink(data, filename)
971 else:
973 else:
972 try:
974 try:
973 if os.path.islink(self.wjoin(filename)):
975 if os.path.islink(self.wjoin(filename)):
974 os.unlink(filename)
976 os.unlink(filename)
975 except OSError:
977 except OSError:
976 pass
978 pass
977 self.wopener(filename, 'w').write(data)
979 self.wopener(filename, 'w').write(data)
978
980
979 if self.is_exec:
981 if self.is_exec:
980 was_exec = self.is_exec(self.wjoin(filename))
982 was_exec = self.is_exec(self.wjoin(filename))
981 else:
983 else:
982 # On filesystems not supporting execute-bit, there is no way
984 # On filesystems not supporting execute-bit, there is no way
983 # to know if it is set but asking subversion. Setting it
985 # to know if it is set but asking subversion. Setting it
984 # systematically is just as expensive and much simpler.
986 # systematically is just as expensive and much simpler.
985 was_exec = 'x' not in flags
987 was_exec = 'x' not in flags
986
988
987 util.set_flags(self.wjoin(filename), flags)
989 util.set_flags(self.wjoin(filename), flags)
988 if was_exec:
990 if was_exec:
989 if 'x' not in flags:
991 if 'x' not in flags:
990 self.delexec.append(filename)
992 self.delexec.append(filename)
991 else:
993 else:
992 if 'x' in flags:
994 if 'x' in flags:
993 self.setexec.append(filename)
995 self.setexec.append(filename)
994
996
995 def delfile(self, name):
997 def delfile(self, name):
996 self.delete.append(name)
998 self.delete.append(name)
997
999
998 def copyfile(self, source, dest):
1000 def copyfile(self, source, dest):
999 self.copies.append([source, dest])
1001 self.copies.append([source, dest])
1000
1002
1001 def _copyfile(self, source, dest):
1003 def _copyfile(self, source, dest):
1002 # SVN's copy command pukes if the destination file exists, but
1004 # SVN's copy command pukes if the destination file exists, but
1003 # our copyfile method expects to record a copy that has
1005 # our copyfile method expects to record a copy that has
1004 # already occurred. Cross the semantic gap.
1006 # already occurred. Cross the semantic gap.
1005 wdest = self.wjoin(dest)
1007 wdest = self.wjoin(dest)
1006 exists = os.path.exists(wdest)
1008 exists = os.path.exists(wdest)
1007 if exists:
1009 if exists:
1008 fd, tempname = tempfile.mkstemp(
1010 fd, tempname = tempfile.mkstemp(
1009 prefix='hg-copy-', dir=os.path.dirname(wdest))
1011 prefix='hg-copy-', dir=os.path.dirname(wdest))
1010 os.close(fd)
1012 os.close(fd)
1011 os.unlink(tempname)
1013 os.unlink(tempname)
1012 os.rename(wdest, tempname)
1014 os.rename(wdest, tempname)
1013 try:
1015 try:
1014 self.run0('copy', source, dest)
1016 self.run0('copy', source, dest)
1015 finally:
1017 finally:
1016 if exists:
1018 if exists:
1017 try:
1019 try:
1018 os.unlink(wdest)
1020 os.unlink(wdest)
1019 except OSError:
1021 except OSError:
1020 pass
1022 pass
1021 os.rename(tempname, wdest)
1023 os.rename(tempname, wdest)
1022
1024
1023 def dirs_of(self, files):
1025 def dirs_of(self, files):
1024 dirs = util.set()
1026 dirs = util.set()
1025 for f in files:
1027 for f in files:
1026 if os.path.isdir(self.wjoin(f)):
1028 if os.path.isdir(self.wjoin(f)):
1027 dirs.add(f)
1029 dirs.add(f)
1028 for i in strutil.rfindall(f, '/'):
1030 for i in strutil.rfindall(f, '/'):
1029 dirs.add(f[:i])
1031 dirs.add(f[:i])
1030 return dirs
1032 return dirs
1031
1033
1032 def add_dirs(self, files):
1034 def add_dirs(self, files):
1033 add_dirs = [d for d in self.dirs_of(files)
1035 add_dirs = [d for d in self.dirs_of(files)
1034 if not os.path.exists(self.wjoin(d, '.svn', 'entries'))]
1036 if not os.path.exists(self.wjoin(d, '.svn', 'entries'))]
1035 if add_dirs:
1037 if add_dirs:
1036 add_dirs.sort()
1038 add_dirs.sort()
1037 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1039 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1038 return add_dirs
1040 return add_dirs
1039
1041
1040 def add_files(self, files):
1042 def add_files(self, files):
1041 if files:
1043 if files:
1042 self.xargs(files, 'add', quiet=True)
1044 self.xargs(files, 'add', quiet=True)
1043 return files
1045 return files
1044
1046
1045 def tidy_dirs(self, names):
1047 def tidy_dirs(self, names):
1046 dirs = list(self.dirs_of(names))
1048 dirs = list(self.dirs_of(names))
1047 dirs.sort()
1049 dirs.sort()
1048 dirs.reverse()
1050 dirs.reverse()
1049 deleted = []
1051 deleted = []
1050 for d in dirs:
1052 for d in dirs:
1051 wd = self.wjoin(d)
1053 wd = self.wjoin(d)
1052 if os.listdir(wd) == '.svn':
1054 if os.listdir(wd) == '.svn':
1053 self.run0('delete', d)
1055 self.run0('delete', d)
1054 deleted.append(d)
1056 deleted.append(d)
1055 return deleted
1057 return deleted
1056
1058
1057 def addchild(self, parent, child):
1059 def addchild(self, parent, child):
1058 self.childmap[parent] = child
1060 self.childmap[parent] = child
1059
1061
1060 def revid(self, rev):
1062 def revid(self, rev):
1061 return u"svn:%s@%s" % (self.uuid, rev)
1063 return u"svn:%s@%s" % (self.uuid, rev)
1062
1064
1063 def putcommit(self, files, parents, commit):
1065 def putcommit(self, files, parents, commit):
1064 for parent in parents:
1066 for parent in parents:
1065 try:
1067 try:
1066 return self.revid(self.childmap[parent])
1068 return self.revid(self.childmap[parent])
1067 except KeyError:
1069 except KeyError:
1068 pass
1070 pass
1069 entries = util.set(self.delete)
1071 entries = util.set(self.delete)
1070 files = util.frozenset(files)
1072 files = util.frozenset(files)
1071 entries.update(self.add_dirs(files.difference(entries)))
1073 entries.update(self.add_dirs(files.difference(entries)))
1072 if self.copies:
1074 if self.copies:
1073 for s, d in self.copies:
1075 for s, d in self.copies:
1074 self._copyfile(s, d)
1076 self._copyfile(s, d)
1075 self.copies = []
1077 self.copies = []
1076 if self.delete:
1078 if self.delete:
1077 self.xargs(self.delete, 'delete')
1079 self.xargs(self.delete, 'delete')
1078 self.delete = []
1080 self.delete = []
1079 entries.update(self.add_files(files.difference(entries)))
1081 entries.update(self.add_files(files.difference(entries)))
1080 entries.update(self.tidy_dirs(entries))
1082 entries.update(self.tidy_dirs(entries))
1081 if self.delexec:
1083 if self.delexec:
1082 self.xargs(self.delexec, 'propdel', 'svn:executable')
1084 self.xargs(self.delexec, 'propdel', 'svn:executable')
1083 self.delexec = []
1085 self.delexec = []
1084 if self.setexec:
1086 if self.setexec:
1085 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1087 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1086 self.setexec = []
1088 self.setexec = []
1087
1089
1088 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1090 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1089 fp = os.fdopen(fd, 'w')
1091 fp = os.fdopen(fd, 'w')
1090 fp.write(commit.desc)
1092 fp.write(commit.desc)
1091 fp.close()
1093 fp.close()
1092 try:
1094 try:
1093 output = self.run0('commit',
1095 output = self.run0('commit',
1094 username=util.shortuser(commit.author),
1096 username=util.shortuser(commit.author),
1095 file=messagefile,
1097 file=messagefile,
1096 encoding='utf-8')
1098 encoding='utf-8')
1097 try:
1099 try:
1098 rev = self.commit_re.search(output).group(1)
1100 rev = self.commit_re.search(output).group(1)
1099 except AttributeError:
1101 except AttributeError:
1100 self.ui.warn(_('unexpected svn output:\n'))
1102 self.ui.warn(_('unexpected svn output:\n'))
1101 self.ui.warn(output)
1103 self.ui.warn(output)
1102 raise util.Abort(_('unable to cope with svn output'))
1104 raise util.Abort(_('unable to cope with svn output'))
1103 if commit.rev:
1105 if commit.rev:
1104 self.run('propset', 'hg:convert-rev', commit.rev,
1106 self.run('propset', 'hg:convert-rev', commit.rev,
1105 revprop=True, revision=rev)
1107 revprop=True, revision=rev)
1106 if commit.branch and commit.branch != 'default':
1108 if commit.branch and commit.branch != 'default':
1107 self.run('propset', 'hg:convert-branch', commit.branch,
1109 self.run('propset', 'hg:convert-branch', commit.branch,
1108 revprop=True, revision=rev)
1110 revprop=True, revision=rev)
1109 for parent in parents:
1111 for parent in parents:
1110 self.addchild(parent, rev)
1112 self.addchild(parent, rev)
1111 return self.revid(rev)
1113 return self.revid(rev)
1112 finally:
1114 finally:
1113 os.unlink(messagefile)
1115 os.unlink(messagefile)
1114
1116
1115 def puttags(self, tags):
1117 def puttags(self, tags):
1116 self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n'))
1118 self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n'))
@@ -1,65 +1,71 b''
1 #!/bin/sh
1 #!/bin/sh
2
2
3 "$TESTDIR/hghave" svn svn-bindings || exit 80
3 "$TESTDIR/hghave" svn svn-bindings || exit 80
4
4
5 fix_path()
5 fix_path()
6 {
6 {
7 tr '\\' /
7 tr '\\' /
8 }
8 }
9
9
10 echo "[extensions]" >> $HGRCPATH
10 echo "[extensions]" >> $HGRCPATH
11 echo "convert = " >> $HGRCPATH
11 echo "convert = " >> $HGRCPATH
12 echo "hgext.graphlog =" >> $HGRCPATH
12 echo "hgext.graphlog =" >> $HGRCPATH
13
13
14 svnadmin create svn-repo
14 svnadmin create svn-repo
15
15
16 svnpath=`pwd | fix_path`
16 svnpath=`pwd | fix_path`
17 # SVN wants all paths to start with a slash. Unfortunately,
17 # SVN wants all paths to start with a slash. Unfortunately,
18 # Windows ones don't. Handle that.
18 # Windows ones don't. Handle that.
19 expr $svnpath : "\/" > /dev/null
19 expr $svnpath : "\/" > /dev/null
20 if [ $? -ne 0 ]; then
20 if [ $? -ne 0 ]; then
21 svnpath='/'$svnpath
21 svnpath='/'$svnpath
22 fi
22 fi
23
23
24 echo % initial svn import
24 echo % initial svn import
25 mkdir projA
25 mkdir projA
26 cd projA
26 cd projA
27 mkdir trunk
27 mkdir trunk
28 echo a > trunk/a
28 echo a > trunk/a
29 mkdir trunk/d1
29 mkdir trunk/d1
30 mkdir trunk/d2
30 echo b > trunk/d1/b
31 echo b > trunk/d1/b
31 echo c > trunk/d1/c
32 echo c > trunk/d1/c
33 echo d > trunk/d2/d
32 cd ..
34 cd ..
33
35
34 svnurl=file://$svnpath/svn-repo/projA
36 svnurl=file://$svnpath/svn-repo/projA
35 svn import -m "init projA" projA $svnurl | fix_path
37 svn import -m "init projA" projA $svnurl | fix_path
36
38
37 # Build a module renaming chain which used to confuse the converter.
39 # Build a module renaming chain which used to confuse the converter.
38 echo % update svn repository
40 echo % update svn repository
39 svn co $svnurl A | fix_path
41 svn co $svnurl A | fix_path
40 cd A
42 cd A
41 "$TESTDIR/svn-safe-append.py" a trunk/a
43 "$TESTDIR/svn-safe-append.py" a trunk/a
42 "$TESTDIR/svn-safe-append.py" c trunk/d1/c
44 "$TESTDIR/svn-safe-append.py" c trunk/d1/c
43 svn ci -m commitbeforemove
45 svn ci -m commitbeforemove
44 svn mv $svnurl/trunk $svnurl/subproject -m movedtrunk
46 svn mv $svnurl/trunk $svnurl/subproject -m movedtrunk
45 svn up
47 svn up
46 mkdir subproject/trunk
48 mkdir subproject/trunk
47 svn add subproject/trunk
49 svn add subproject/trunk
48 svn ci -m createtrunk
50 svn ci -m createtrunk
49 mkdir subproject/branches
51 mkdir subproject/branches
50 svn add subproject/branches
52 svn add subproject/branches
51 svn ci -m createbranches
53 svn ci -m createbranches
52 svn mv $svnurl/subproject/d1 $svnurl/subproject/trunk/d1 -m moved1
54 svn mv $svnurl/subproject/d1 $svnurl/subproject/trunk/d1 -m moved1
55 svn mv $svnurl/subproject/d2 $svnurl/subproject/trunk/d2 -m moved2
53 svn up
56 svn up
54 "$TESTDIR/svn-safe-append.py" b subproject/trunk/d1/b
57 "$TESTDIR/svn-safe-append.py" b subproject/trunk/d1/b
55 svn ci -m changeb
58 svn rm subproject/trunk/d2
59 svn ci -m "changeb and rm d2"
56 svn mv $svnurl/subproject/trunk/d1 $svnurl/subproject/branches/d1 -m moved1again
60 svn mv $svnurl/subproject/trunk/d1 $svnurl/subproject/branches/d1 -m moved1again
61 echo % copy a directory from a past revision
62 svn copy -r 7 $svnurl/subproject/trunk/d2 $svnurl/subproject/trunk -m copydirfrompast
57 cd ..
63 cd ..
58
64
59 echo % convert trunk and branches
65 echo % convert trunk and branches
60 hg convert --datesort $svnurl/subproject A-hg
66 hg convert --datesort $svnurl/subproject A-hg
61
67
62 cd A-hg
68 cd A-hg
63 hg glog --template '#rev# #desc|firstline# files: #files#\n'
69 hg glog --template '#rev# #desc|firstline# files: #files#\n'
64 hg branches | sed 's/:.*/:/'
70 hg branches | sed 's/:.*/:/'
65 cd ..
71 cd ..
@@ -1,76 +1,99 b''
1 % initial svn import
1 % initial svn import
2 Adding projA/trunk
2 Adding projA/trunk
3 Adding projA/trunk/a
3 Adding projA/trunk/a
4 Adding projA/trunk/d1
4 Adding projA/trunk/d1
5 Adding projA/trunk/d1/b
5 Adding projA/trunk/d1/b
6 Adding projA/trunk/d1/c
6 Adding projA/trunk/d1/c
7 Adding projA/trunk/d2
8 Adding projA/trunk/d2/d
7
9
8 Committed revision 1.
10 Committed revision 1.
9 % update svn repository
11 % update svn repository
10 A A/trunk
12 A A/trunk
11 A A/trunk/a
13 A A/trunk/a
12 A A/trunk/d1
14 A A/trunk/d1
13 A A/trunk/d1/b
15 A A/trunk/d1/b
14 A A/trunk/d1/c
16 A A/trunk/d1/c
17 A A/trunk/d2
18 A A/trunk/d2/d
15 Checked out revision 1.
19 Checked out revision 1.
16 Sending trunk/a
20 Sending trunk/a
17 Sending trunk/d1/c
21 Sending trunk/d1/c
18 Transmitting file data ..
22 Transmitting file data ..
19 Committed revision 2.
23 Committed revision 2.
20
24
21 Committed revision 3.
25 Committed revision 3.
22 D trunk
26 D trunk
23 A subproject
27 A subproject
24 A subproject/a
28 A subproject/a
25 A subproject/d1
29 A subproject/d1
26 A subproject/d1/b
30 A subproject/d1/b
27 A subproject/d1/c
31 A subproject/d1/c
32 A subproject/d2
33 A subproject/d2/d
28 Updated to revision 3.
34 Updated to revision 3.
29 A subproject/trunk
35 A subproject/trunk
30 Adding subproject/trunk
36 Adding subproject/trunk
31
37
32 Committed revision 4.
38 Committed revision 4.
33 A subproject/branches
39 A subproject/branches
34 Adding subproject/branches
40 Adding subproject/branches
35
41
36 Committed revision 5.
42 Committed revision 5.
37
43
38 Committed revision 6.
44 Committed revision 6.
45
46 Committed revision 7.
39 A subproject/trunk/d1
47 A subproject/trunk/d1
40 A subproject/trunk/d1/b
48 A subproject/trunk/d1/b
41 A subproject/trunk/d1/c
49 A subproject/trunk/d1/c
50 A subproject/trunk/d2
51 A subproject/trunk/d2/d
42 D subproject/d1
52 D subproject/d1
43 Updated to revision 6.
53 D subproject/d2
54 Updated to revision 7.
55 D subproject/trunk/d2/d
56 D subproject/trunk/d2
44 Sending subproject/trunk/d1/b
57 Sending subproject/trunk/d1/b
58 Deleting subproject/trunk/d2
45 Transmitting file data .
59 Transmitting file data .
46 Committed revision 7.
60 Committed revision 8.
47
61
48 Committed revision 8.
62 Committed revision 9.
63 % copy a directory from a past revision
64
65 Committed revision 10.
49 % convert trunk and branches
66 % convert trunk and branches
50 initializing destination A-hg repository
67 initializing destination A-hg repository
51 scanning source...
68 scanning source...
52 sorting...
69 sorting...
53 converting...
70 converting...
54 6 createtrunk
71 8 createtrunk
55 5 moved1
72 7 moved1
56 4 moved1
73 6 moved1
57 3 changeb
74 5 moved2
58 2 changeb
75 4 changeb and rm d2
76 3 changeb and rm d2
77 2 moved1again
59 1 moved1again
78 1 moved1again
60 0 moved1again
79 0 copydirfrompast
61 o 6 moved1again files: d1/b d1/c
80 o 8 copydirfrompast files: d2/d
81 |
82 o 7 moved1again files: d1/b d1/c
62 |
83 |
63 | o 5 moved1again files:
84 | o 6 moved1again files:
85 | |
86 o | 5 changeb and rm d2 files: d1/b d2/d
64 | |
87 | |
65 o | 4 changeb files: d1/b
88 | o 4 changeb and rm d2 files: b
66 | |
89 | |
67 | o 3 changeb files: b
90 o | 3 moved2 files: d2/d
68 | |
91 | |
69 o | 2 moved1 files: d1/b d1/c
92 o | 2 moved1 files: d1/b d1/c
70 | |
93 | |
71 | o 1 moved1 files: b c
94 | o 1 moved1 files: b c
72 |
95 |
73 o 0 createtrunk files:
96 o 0 createtrunk files:
74
97
75 default 6:
98 default 8:
76 d1 5:
99 d1 6:
General Comments 0
You need to be logged in to leave comments. Login now