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