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