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