comparison svnwrap/svn_swig_wrapper.py @ 304:ce676eff002b

First merge, totally untested.
author Dan Villiom Podlaski Christiansen <danchr@gmail.com>
date Fri, 01 May 2009 10:28:59 +0200
parents 79440ed81011 25d843281127
children 97360cb777a8
comparison
equal deleted inserted replaced
303:f423a8780832 304:ce676eff002b
2 import getpass 2 import getpass
3 import os 3 import os
4 import shutil 4 import shutil
5 import sys 5 import sys
6 import tempfile 6 import tempfile
7 import urlparse
8 import urllib
7 import hashlib 9 import hashlib
8 import collections 10 import collections
9 import gc
10 11
11 from svn import client 12 from svn import client
12 from svn import core 13 from svn import core
13 from svn import delta 14 from svn import delta
14 from svn import ra 15 from svn import ra
68 def _create_auth_baton(pool): 69 def _create_auth_baton(pool):
69 """Create a Subversion authentication baton. """ 70 """Create a Subversion authentication baton. """
70 # Give the client context baton a suite of authentication 71 # Give the client context baton a suite of authentication
71 # providers.h 72 # providers.h
72 platform_specific = ['svn_auth_get_gnome_keyring_simple_provider', 73 platform_specific = ['svn_auth_get_gnome_keyring_simple_provider',
73 'svn_auth_get_gnome_keyring_ssl_client_cert_pw_provider', 74 'svn_auth_get_gnome_keyring_ssl_client_cert_pw_provider',
74 'svn_auth_get_keychain_simple_provider', 75 'svn_auth_get_keychain_simple_provider',
75 'svn_auth_get_keychain_ssl_client_cert_pw_provider', 76 'svn_auth_get_keychain_ssl_client_cert_pw_provider',
76 'svn_auth_get_kwallet_simple_provider', 77 'svn_auth_get_kwallet_simple_provider',
77 'svn_auth_get_kwallet_ssl_client_cert_pw_provider', 78 'svn_auth_get_kwallet_ssl_client_cert_pw_provider',
78 'svn_auth_get_ssl_client_cert_file_provider', 79 'svn_auth_get_ssl_client_cert_file_provider',
79 'svn_auth_get_windows_simple_provider', 80 'svn_auth_get_windows_simple_provider',
80 'svn_auth_get_windows_ssl_server_trust_provider', 81 'svn_auth_get_windows_ssl_server_trust_provider',
81 ] 82 ]
82 83
83 providers = [] 84 providers = []
84 85 # Platform-dependant authentication methods
85 for p in platform_specific: 86 getprovider = getattr(core, 'svn_auth_get_platform_specific_provider',
86 if hasattr(core, p): 87 None)
87 try: 88 if getprovider:
88 providers.append(getattr(core, p)()) 89 # Available in svn >= 1.6
89 except RuntimeError: 90 for name in ('gnome_keyring', 'keychain', 'kwallet', 'windows'):
90 pass 91 for type in ('simple', 'ssl_client_cert_pw', 'ssl_server_trust'):
92 p = getprovider(name, type, pool)
93 if p:
94 providers.append(p)
95 else:
96 for p in platform_specific:
97 if hasattr(core, p):
98 try:
99 providers.append(getattr(core, p)())
100 except RuntimeError:
101 pass
91 102
92 providers += [ 103 providers += [
93 client.get_simple_provider(), 104 client.get_simple_provider(),
94 client.get_username_provider(), 105 client.get_username_provider(),
95 client.get_ssl_client_cert_file_provider(), 106 client.get_ssl_client_cert_file_provider(),
98 client.get_simple_prompt_provider(user_pass_prompt, 2), 109 client.get_simple_prompt_provider(user_pass_prompt, 2),
99 ] 110 ]
100 111
101 return core.svn_auth_open(providers, pool) 112 return core.svn_auth_open(providers, pool)
102 113
114 def parse_url(url):
115 """Parse a URL and return a tuple (username, password, url)
116 """
117 scheme, netloc, path, params, query, fragment = urlparse.urlparse(url)
118 user, passwd = None, None
119 if '@' in netloc:
120 userpass, netloc = netloc.split('@')
121 if ':' in userpass:
122 user, passwd = userpass.split(':')
123 user, passwd = urllib.unquote(user) or None, urllib.unquote(passwd) or None
124 else:
125 user = urllib.unquote(userpass) or None
126 url = urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
127 return (user, passwd, url)
103 128
104 class Revision(tuple): 129 class Revision(tuple):
105 """Wrapper for a Subversion revision. 130 """Wrapper for a Subversion revision.
106 131
107 Derives from tuple in an attempt to minimise the memory footprint. 132 Derives from tuple in an attempt to minimise the memory footprint.
146 """Wrapper for a Subversion repository. 171 """Wrapper for a Subversion repository.
147 172
148 This uses the SWIG Python bindings, and will only work on svn >= 1.4. 173 This uses the SWIG Python bindings, and will only work on svn >= 1.4.
149 It takes a required param, the URL. 174 It takes a required param, the URL.
150 """ 175 """
151 def __init__(self, url='', username='', head=None): 176 def __init__(self, url='', username='', password='', head=None):
152 self.svn_url = url 177 parsed = parse_url(url)
153 self.uname = username 178 # --username and --password override URL credentials
179 self.username = username or parsed[0]
180 self.password = password or parsed[1]
181 self.svn_url = parsed[2]
154 self.auth_baton_pool = core.Pool() 182 self.auth_baton_pool = core.Pool()
155 self.auth_baton = _create_auth_baton(self.auth_baton_pool) 183 self.auth_baton = _create_auth_baton(self.auth_baton_pool)
156 # self.init_ra_and_client() assumes that a pool already exists 184 # self.init_ra_and_client() assumes that a pool already exists
157 self.pool = core.Pool() 185 self.pool = core.Pool()
158 186
167 195
168 def init_ra_and_client(self): 196 def init_ra_and_client(self):
169 """Initializes the RA and client layers, because sometimes getting 197 """Initializes the RA and client layers, because sometimes getting
170 unified diffs runs the remote server out of open files. 198 unified diffs runs the remote server out of open files.
171 """ 199 """
172 # Debugging code; retained for possible later use. 200 # while we're in here we'll recreate our pool
173 if False:
174 gc.collect()
175 import pympler.muppy.tracker
176 try:
177 self.memory_tracker
178 try:
179 self.memory_tracker.print_diff(self.memory_base)
180 except:
181 print 'HOP'
182 self.memory_base = self.memory_tracker.create_summary()
183 except:
184 print 'HEP'
185 self.memory_tracker = pympler.muppy.tracker.SummaryTracker()
186
187 # while we're in here we'll recreate our pool, but first, we clear it
188 # and destroy it to make possible leaks cause fatal errors.
189 self.pool.clear()
190 self.pool.destroy()
191 self.pool = core.Pool() 201 self.pool = core.Pool()
202 if self.username:
203 core.svn_auth_set_parameter(self.auth_baton,
204 core.SVN_AUTH_PARAM_DEFAULT_USERNAME,
205 self.username)
206 if self.password:
207 core.svn_auth_set_parameter(self.auth_baton,
208 core.SVN_AUTH_PARAM_DEFAULT_PASSWORD,
209 self.password)
192 self.client_context = client.create_context() 210 self.client_context = client.create_context()
193 211
194 self.client_context.auth_baton = self.auth_baton 212 self.client_context.auth_baton = self.auth_baton
195 self.client_context.config = svn_config 213 self.client_context.config = svn_config
196 callbacks = RaCallbacks() 214 callbacks = RaCallbacks()
296 if hist.paths[path].copyfrom_path is None: 314 if hist.paths[path].copyfrom_path is None:
297 return None, None 315 return None, None
298 source = hist.paths[path].copyfrom_path 316 source = hist.paths[path].copyfrom_path
299 source_rev = 0 317 source_rev = 0
300 for p in hist.paths: 318 for p in hist.paths:
319 if not p.startswith(path):
320 continue
301 if hist.paths[p].copyfrom_rev: 321 if hist.paths[p].copyfrom_rev:
302 # We assume that the revision of the source tree as it was 322 # We assume that the revision of the source tree as it was
303 # copied was actually the revision of the highest revision 323 # copied was actually the revision of the highest revision
304 # copied item. This could be wrong, but in practice it will 324 # copied item. This could be wrong, but in practice it will
305 # *probably* be correct 325 # *probably* be correct
332 of revisions at a time. 352 of revisions at a time.
333 353
334 The reason this is lazy is so that you can use the same repo object 354 The reason this is lazy is so that you can use the same repo object
335 to perform RA calls to get deltas. 355 to perform RA calls to get deltas.
336 """ 356 """
337 # NB: you'd think this would work, but you'd be wrong. I'm pretty 357 return self.fetch_history_at_paths([''], start=start,
338 # convinced there must be some kind of svn bug here.
339 #return self.fetch_history_at_paths(['tags', 'trunk', 'branches'],
340 # start=start)
341 # However, we no longer need such filtering, as we gracefully handle
342 # branches located at arbitrary locations.
343 return self.fetch_history_at_paths([''], start=start, stop=stop,
344 chunk_size=chunk_size) 358 chunk_size=chunk_size)
345 359
346 def fetch_history_at_paths(self, paths, start=None, stop=None, 360 def fetch_history_at_paths(self, paths, start=None, stop=None,
347 chunk_size=_chunk_size): 361 chunk_size=_chunk_size):
348 '''TODO: This method should be merged with self.revisions() as 362 '''TODO: This method should be merged with self.revisions() as
410 False, 424 False,
411 self.pool) 425 self.pool)
412 checksum = [] 426 checksum = []
413 # internal dir batons can fall out of scope and get GCed before svn is 427 # internal dir batons can fall out of scope and get GCed before svn is
414 # done with them. This prevents that (credit to gvn for the idea). 428 # done with them. This prevents that (credit to gvn for the idea).
415 # TODO: verify that these are not the cause of our leaks
416 batons = [edit_baton, ] 429 batons = [edit_baton, ]
417 def driver_cb(parent, path, pool): 430 def driver_cb(parent, path, pool):
418 if not parent: 431 if not parent:
419 bat = editor.open_root(edit_baton, base_revision, self.pool) 432 bat = editor.open_root(edit_baton, base_revision, self.pool)
420 batons.append(bat) 433 batons.append(bat)
437 base_text, new_text, action = file_data[path] 450 base_text, new_text, action = file_data[path]
438 compute_delta = True 451 compute_delta = True
439 if action == 'modify': 452 if action == 'modify':
440 baton = editor.open_file(path, parent, base_revision, pool) 453 baton = editor.open_file(path, parent, base_revision, pool)
441 elif action == 'add': 454 elif action == 'add':
442 try: 455 frompath, fromrev = copies.get(path, (None, -1))
443 frompath, fromrev = copies.get(path, (None, -1)) 456 if frompath:
444 if frompath: 457 frompath = self.svn_url + '/' + frompath
445 frompath = self.svn_url + '/' + frompath 458 baton = editor.add_file(path, parent, frompath, fromrev, pool)
446 baton = editor.add_file(path, parent, frompath, fromrev, pool)
447 except (core.SubversionException, TypeError), e: #pragma: no cover
448 print e
449 raise
450 elif action == 'delete': 459 elif action == 'delete':
451 baton = editor.delete_entry(path, base_revision, parent, pool) 460 baton = editor.delete_entry(path, base_revision, parent, pool)
452 compute_delta = False 461 compute_delta = False
453 462
454 if path in properties: 463 if path in properties:
473 delta.path_driver(editor, edit_baton, base_revision, paths, driver_cb, 482 delta.path_driver(editor, edit_baton, base_revision, paths, driver_cb,
474 self.pool) 483 self.pool)
475 editor.close_edit(edit_baton, self.pool) 484 editor.close_edit(edit_baton, self.pool)
476 485
477 def get_replay(self, revision, editor, oldest_rev_i_have=0): 486 def get_replay(self, revision, editor, oldest_rev_i_have=0):
487 # this method has a tendency to chew through RAM if you don't re-init
488 self.init_ra_and_client()
478 e_ptr, e_baton = delta.make_editor(editor) 489 e_ptr, e_baton = delta.make_editor(editor)
479 try: 490 try:
480 ra.replay(self.ra, revision, oldest_rev_i_have, True, e_ptr, 491 ra.replay(self.ra, revision, oldest_rev_i_have, True, e_ptr,
481 e_baton, self.pool) 492 e_baton, self.pool)
482 except core.SubversionException, e: #pragma: no cover 493 except core.SubversionException, e: #pragma: no cover
494 # can I depend on this number being constant?
483 if (e.apr_err == core.SVN_ERR_RA_NOT_IMPLEMENTED or 495 if (e.apr_err == core.SVN_ERR_RA_NOT_IMPLEMENTED or
484 e.apr_err == core.SVN_ERR_UNSUPPORTED_FEATURE): 496 e.apr_err == core.SVN_ERR_UNSUPPORTED_FEATURE):
485 raise SubversionRepoCanNotReplay, ('This Subversion server ' 497 raise SubversionRepoCanNotReplay, ('This Subversion server '
486 'is older than 1.4.0, and cannot satisfy replay requests.') 498 'is older than 1.4.0, and cannot satisfy replay requests.')
487 else: 499 else:
491 deleted=True, ignore_type=False): 503 deleted=True, ignore_type=False):
492 """Gets a unidiff of path at revision against revision-1. 504 """Gets a unidiff of path at revision against revision-1.
493 """ 505 """
494 if not self.hasdiff3: 506 if not self.hasdiff3:
495 raise SubversionRepoCanNotDiff() 507 raise SubversionRepoCanNotDiff()
508 # works around an svn server keeping too many open files (observed
509 # in an svnserve from the 1.2 era)
510 self.init_ra_and_client()
496 511
497 assert path[0] != '/' 512 assert path[0] != '/'
498 url = self.svn_url + '/' + path 513 url = self.svn_url + '/' + path
499 url2 = url 514 url2 = url
500 if other_path is not None: 515 if other_path is not None:
554 mode = ("svn:special" in info) and 'l' or mode 569 mode = ("svn:special" in info) and 'l' or mode
555 except core.SubversionException, e: 570 except core.SubversionException, e:
556 notfound = (core.SVN_ERR_FS_NOT_FOUND, 571 notfound = (core.SVN_ERR_FS_NOT_FOUND,
557 core.SVN_ERR_RA_DAV_PATH_NOT_FOUND) 572 core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
558 if e.apr_err in notfound: # File not found 573 if e.apr_err in notfound: # File not found
559 raise IOError, e.args[0] 574 raise IOError()
560 raise 575 raise
561 if mode == 'l': 576 if mode == 'l':
562 linkprefix = "link " 577 linkprefix = "link "
563 if data.startswith(linkprefix): 578 if data.startswith(linkprefix):
564 data = data[len(linkprefix):] 579 data = data[len(linkprefix):]
596 'dirpath' and 'kind' is 'f' if the entry is a file, 'd' if it is a 611 'dirpath' and 'kind' is 'f' if the entry is a file, 'd' if it is a
597 directory. Raise IOError if the directory cannot be found at given 612 directory. Raise IOError if the directory cannot be found at given
598 revision. 613 revision.
599 """ 614 """
600 dirpath = dirpath.strip('/') 615 dirpath = dirpath.strip('/')
616 pool = core.Pool()
601 rpath = '/'.join([self.svn_url, dirpath]).strip('/') 617 rpath = '/'.join([self.svn_url, dirpath]).strip('/')
602 rev = optrev(revision) 618 rev = optrev(revision)
603 try: 619 try:
604 entries = client.ls(rpath, rev, True, self.client_context, 620 entries = client.ls(rpath, rev, True, self.client_context, pool)
605 self.pool)
606 except core.SubversionException, e: 621 except core.SubversionException, e:
607 if e.apr_err == core.SVN_ERR_FS_NOT_FOUND: 622 if e.apr_err == core.SVN_ERR_FS_NOT_FOUND:
608 raise IOError('%s cannot be found at r%d' % (dirpath, revision)) 623 raise IOError('%s cannot be found at r%d' % (dirpath, revision))
609 raise 624 raise
610 for path, e in entries.iteritems(): 625 for path, e in entries.iteritems():