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