Mercurial > hgsubversion
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(): |
