Mercurial > hgsubversion
comparison svnwrap/svn_swig_wrapper.py @ 0:f2636cfed115
Initial import of hgsubversion into a public repository.
| author | Augie Fackler <durin42@gmail.com> |
|---|---|
| date | Tue, 30 Sep 2008 11:42:52 -0500 |
| parents | |
| children | 1a5bb173170b |
comparison
equal
deleted
inserted
replaced
| -1:000000000000 | 0:f2636cfed115 |
|---|---|
| 1 import cStringIO | |
| 2 import getpass | |
| 3 import os | |
| 4 import pwd | |
| 5 import shutil | |
| 6 import sys | |
| 7 import tempfile | |
| 8 | |
| 9 from svn import client | |
| 10 from svn import core | |
| 11 from svn import delta | |
| 12 from svn import ra | |
| 13 | |
| 14 svn_config = core.svn_config_get_config(None) | |
| 15 | |
| 16 | |
| 17 def user_pass_prompt(realm, default_username, ms, pool): | |
| 18 creds = core.svn_auth_cred_simple_t() | |
| 19 creds.may_save = ms | |
| 20 if default_username: | |
| 21 sys.stderr.write('Auth realm: %s\n' % (realm,)) | |
| 22 creds.username = default_username | |
| 23 else: | |
| 24 sys.stderr.write('Auth realm: %s\n' % (realm,)) | |
| 25 sys.stderr.write('Username: ') | |
| 26 sys.stderr.flush() | |
| 27 creds.username = sys.stdin.readline().strip() | |
| 28 creds.password = getpass.getpass('Password for %s: ' % creds.username) | |
| 29 return creds | |
| 30 | |
| 31 def _create_auth_baton(pool): | |
| 32 """Create a Subversion authentication baton. """ | |
| 33 # Give the client context baton a suite of authentication | |
| 34 # providers.h | |
| 35 providers = [ | |
| 36 client.get_simple_provider(), | |
| 37 client.get_username_provider(), | |
| 38 client.get_ssl_client_cert_file_provider(), | |
| 39 client.get_ssl_client_cert_pw_file_provider(), | |
| 40 client.get_ssl_server_trust_file_provider(), | |
| 41 ] | |
| 42 # Platform-dependant authentication methods | |
| 43 if hasattr(client, 'get_windows_simple_provider'): | |
| 44 providers.append(client.get_windows_simple_provider()) | |
| 45 if hasattr(client, 'get_keychain_simple_provider'): | |
| 46 providers.append(client.get_keychain_simple_provider()) | |
| 47 providers.extend([client.get_simple_prompt_provider(user_pass_prompt, 2), | |
| 48 ]) | |
| 49 return core.svn_auth_open(providers, pool) | |
| 50 | |
| 51 | |
| 52 class Revision(object): | |
| 53 """Wrapper for a Subversion revision. | |
| 54 """ | |
| 55 def __init__(self, revnum, author, message, date, paths, strip_path=''): | |
| 56 self.revnum, self.author, self.message = revnum, author, message | |
| 57 # TODO parse this into a datetime | |
| 58 self.date = date | |
| 59 self.paths = {} | |
| 60 for p in paths: | |
| 61 self.paths[p[len(strip_path):]] = paths[p] | |
| 62 | |
| 63 def __str__(self): | |
| 64 return 'r%d by %s' % (self.revnum, self.author) | |
| 65 | |
| 66 class SubversionRepo(object): | |
| 67 """Wrapper for a Subversion repository. | |
| 68 | |
| 69 This uses the SWIG Python bindings, and will only work on svn >= 1.4. | |
| 70 It takes a required param, the URL. | |
| 71 """ | |
| 72 def __init__(self, url=''): | |
| 73 self.svn_url = url | |
| 74 self.auth_baton_pool = core.Pool() | |
| 75 self.auth_baton = _create_auth_baton(self.auth_baton_pool) | |
| 76 | |
| 77 self.init_ra_and_client() | |
| 78 self.uuid = ra.get_uuid(self.ra, self.pool) | |
| 79 repo_root = ra.get_repos_root(self.ra, self.pool) | |
| 80 # *will* have a leading '/', would not if we used get_repos_root2 | |
| 81 self.subdir = url[len(repo_root):] | |
| 82 if not self.subdir or self.subdir[-1] != '/': | |
| 83 self.subdir += '/' | |
| 84 | |
| 85 def init_ra_and_client(self): | |
| 86 """Initializes the RA and client layers, because sometimes getting | |
| 87 unified diffs runs the remote server out of open files. | |
| 88 """ | |
| 89 # while we're in here we'll recreate our pool | |
| 90 self.pool = core.Pool() | |
| 91 self.client_context = client.create_context() | |
| 92 self.uname = str(pwd.getpwuid(os.getuid())[0]) | |
| 93 core.svn_auth_set_parameter(self.auth_baton, | |
| 94 core.SVN_AUTH_PARAM_DEFAULT_USERNAME, | |
| 95 self.uname) | |
| 96 | |
| 97 self.client_context.auth_baton = self.auth_baton | |
| 98 self.client_context.config = svn_config | |
| 99 self.ra = client.open_ra_session(self.svn_url.encode('utf8'), | |
| 100 self.client_context) | |
| 101 | |
| 102 | |
| 103 @property | |
| 104 def HEAD(self): | |
| 105 return ra.get_latest_revnum(self.ra, self.pool) | |
| 106 | |
| 107 @property | |
| 108 def START(self): | |
| 109 return 0 | |
| 110 | |
| 111 @property | |
| 112 def branches(self): | |
| 113 """Get the branches defined in this repo assuming a standard layout. | |
| 114 """ | |
| 115 branches = self.list_dir('branches').keys() | |
| 116 branch_info = {} | |
| 117 head=self.HEAD | |
| 118 for b in branches: | |
| 119 b_path = 'branches/%s' %b | |
| 120 hist_gen = self.fetch_history_at_paths([b_path], stop=head) | |
| 121 hist = hist_gen.next() | |
| 122 source, source_rev = self._get_copy_source(b_path, cached_head=head) | |
| 123 # This if statement guards against projects that have non-ancestral | |
| 124 # branches by not listing them has branches | |
| 125 # Note that they probably are really ancestrally related, but there | |
| 126 # is just no way for us to know how. | |
| 127 if source is not None and source_rev is not None: | |
| 128 branch_info[b] = (source, source_rev, hist.revnum) | |
| 129 return branch_info | |
| 130 | |
| 131 @property | |
| 132 def tags(self): | |
| 133 """Get the current tags in this repo assuming a standard layout. | |
| 134 | |
| 135 This returns a dictionary of tag: (source path, source rev) | |
| 136 """ | |
| 137 tags = self.list_dir('tags').keys() | |
| 138 tag_info = {} | |
| 139 head = self.HEAD | |
| 140 for t in tags: | |
| 141 tag_info[t] = self._get_copy_source('tags/%s' % t, | |
| 142 cached_head=head) | |
| 143 return tag_info | |
| 144 | |
| 145 def _get_copy_source(self, path, cached_head=None): | |
| 146 """Get copy revision for the given path, assuming it was meant to be | |
| 147 a copy of the entire tree. | |
| 148 """ | |
| 149 if not cached_head: | |
| 150 cached_head = self.HEAD | |
| 151 hist_gen = self.fetch_history_at_paths([path], stop=cached_head) | |
| 152 hist = hist_gen.next() | |
| 153 if hist.paths[path].copyfrom_path is None: | |
| 154 return None, None | |
| 155 source = hist.paths[path].copyfrom_path | |
| 156 source_rev = 0 | |
| 157 for p in hist.paths: | |
| 158 if hist.paths[p].copyfrom_rev: | |
| 159 # We assume that the revision of the source tree as it was | |
| 160 # copied was actually the revision of the highest revision | |
| 161 # copied item. This could be wrong, but in practice it will | |
| 162 # *probably* be correct | |
| 163 if source_rev < hist.paths[p].copyfrom_rev: | |
| 164 source_rev = hist.paths[p].copyfrom_rev | |
| 165 source = source[len(self.subdir):] | |
| 166 return source, source_rev | |
| 167 | |
| 168 def list_dir(self, dir, revision=None): | |
| 169 """List the contents of a server-side directory. | |
| 170 | |
| 171 Returns a dict-like object with one dict key per directory entry. | |
| 172 | |
| 173 Args: | |
| 174 dir: the directory to list, no leading slash | |
| 175 rev: the revision at which to list the directory, defaults to HEAD | |
| 176 """ | |
| 177 if dir[-1] == '/': | |
| 178 dir = dir[:-1] | |
| 179 if revision is None: | |
| 180 revision = self.HEAD | |
| 181 r = ra.get_dir2(self.ra, dir, revision, core.SVN_DIRENT_KIND, self.pool) | |
| 182 folders, props, junk = r | |
| 183 return folders | |
| 184 | |
| 185 def revisions(self, start=None, chunk_size=1000): | |
| 186 """Load the history of this repo. | |
| 187 | |
| 188 This is LAZY. It returns a generator, and fetches a small number | |
| 189 of revisions at a time. | |
| 190 | |
| 191 The reason this is lazy is so that you can use the same repo object | |
| 192 to perform RA calls to get deltas. | |
| 193 """ | |
| 194 # NB: you'd think this would work, but you'd be wrong. I'm pretty | |
| 195 # convinced there must be some kind of svn bug here. | |
| 196 #return self.fetch_history_at_paths(['tags', 'trunk', 'branches'], | |
| 197 # start=start) | |
| 198 # this does the same thing, but at the repo root + filtering. It's | |
| 199 # kind of tough cookies, sadly. | |
| 200 for r in self.fetch_history_at_paths([''], start=start, | |
| 201 chunk_size=chunk_size): | |
| 202 should_yield = False | |
| 203 i = 0 | |
| 204 paths = list(r.paths.keys()) | |
| 205 while i < len(paths) and not should_yield: | |
| 206 p = paths[i] | |
| 207 if (p.startswith('trunk') or p.startswith('tags') | |
| 208 or p.startswith('branches')): | |
| 209 should_yield = True | |
| 210 i += 1 | |
| 211 if should_yield: | |
| 212 yield r | |
| 213 | |
| 214 | |
| 215 def fetch_history_at_paths(self, paths, start=None, stop=None, | |
| 216 chunk_size=1000): | |
| 217 revisions = [] | |
| 218 def callback(paths, revnum, author, date, message, pool): | |
| 219 r = Revision(revnum, author, message, date, paths, | |
| 220 strip_path=self.subdir) | |
| 221 revisions.append(r) | |
| 222 if not start: | |
| 223 start = self.START | |
| 224 if not stop: | |
| 225 stop = self.HEAD | |
| 226 while stop > start: | |
| 227 ra.get_log(self.ra, paths, | |
| 228 start+1, | |
| 229 stop, | |
| 230 chunk_size, #limit of how many log messages to load | |
| 231 True, # don't need to know changed paths | |
| 232 True, # stop on copies | |
| 233 callback, | |
| 234 self.pool) | |
| 235 if len(revisions) < chunk_size: | |
| 236 # this means there was no history for the path, so force the | |
| 237 # loop to exit | |
| 238 start = stop | |
| 239 else: | |
| 240 start = revisions[-1].revnum | |
| 241 while len(revisions) > 0: | |
| 242 yield revisions[0] | |
| 243 revisions.pop(0) | |
| 244 | |
| 245 def commit(self, paths, message, file_data, base_revision, dirs): | |
| 246 """Commits the appropriate targets from revision in editor's store. | |
| 247 """ | |
| 248 self.init_ra_and_client() | |
| 249 commit_info = [] | |
| 250 def commit_cb(_commit_info, pool): | |
| 251 commit_info.append(_commit_info) | |
| 252 editor, edit_baton = ra.get_commit_editor2(self.ra, | |
| 253 message, | |
| 254 commit_cb, | |
| 255 None, | |
| 256 False) | |
| 257 checksum = [] | |
| 258 def driver_cb(parent, path, pool): | |
| 259 if path in dirs: | |
| 260 return baton | |
| 261 base_text, new_text, action = file_data[path] | |
| 262 compute_delta = True | |
| 263 if action == 'modify': | |
| 264 baton = editor.open_file(path, parent, base_revision, pool) | |
| 265 elif action == 'add': | |
| 266 try: | |
| 267 baton = editor.add_file(path, parent, None, -1, pool) | |
| 268 except (core.SubversionException, TypeError), e: | |
| 269 print e.message | |
| 270 raise | |
| 271 elif action == 'delete': | |
| 272 baton = editor.delete_entry(path, base_revision, parent, pool) | |
| 273 compute_delta = False | |
| 274 | |
| 275 if compute_delta: | |
| 276 handler, wh_baton = editor.apply_textdelta(baton, None, | |
| 277 self.pool) | |
| 278 | |
| 279 txdelta_stream = delta.svn_txdelta( | |
| 280 cStringIO.StringIO(base_text), cStringIO.StringIO(new_text), | |
| 281 self.pool) | |
| 282 delta.svn_txdelta_send_txstream(txdelta_stream, handler, | |
| 283 wh_baton, pool) | |
| 284 | |
| 285 delta.path_driver(editor, edit_baton, base_revision, paths, driver_cb, | |
| 286 self.pool) | |
| 287 editor.close_edit(edit_baton, self.pool) | |
| 288 | |
| 289 def get_replay(self, revision, editor, oldest_rev_i_have=0): | |
| 290 # this method has a tendency to chew through RAM if you don't re-init | |
| 291 self.init_ra_and_client() | |
| 292 e_ptr, e_baton = delta.make_editor(editor) | |
| 293 try: | |
| 294 ra.replay(self.ra, revision, oldest_rev_i_have, True, e_ptr, | |
| 295 e_baton, self.pool) | |
| 296 except core.SubversionException, e: | |
| 297 # can I depend on this number being constant? | |
| 298 if (e.message == "Server doesn't support the replay command" | |
| 299 or e.apr_err == 170003): | |
| 300 raise SubversionRepoCanNotReplay, ('This Subversion server ' | |
| 301 'is older than 1.4.0, and cannot satisfy replay requests.') | |
| 302 else: | |
| 303 raise | |
| 304 | |
| 305 def get_unified_diff(self, path, revision, other_path=None, other_rev=None, | |
| 306 deleted=True, ignore_type=False): | |
| 307 """Gets a unidiff of path at revision against revision-1. | |
| 308 """ | |
| 309 # works around an svn server keeping too many open files (observed | |
| 310 # in an svnserve from the 1.2 era) | |
| 311 self.init_ra_and_client() | |
| 312 | |
| 313 old_cwd = os.getcwd() | |
| 314 assert path[0] != '/' | |
| 315 url = self.svn_url + '/' + path | |
| 316 url2 = url | |
| 317 if other_path is not None: | |
| 318 url2 = self.svn_url + '/' + other_path | |
| 319 if other_rev is None: | |
| 320 other_rev = revision - 1 | |
| 321 tmpdir = tempfile.mkdtemp('svnwrap_temp') | |
| 322 # hot tip: the swig bridge doesn't like StringIO for these bad boys | |
| 323 out_path = os.path.join(tmpdir, 'diffout') | |
| 324 error_path = os.path.join(tmpdir, 'differr') | |
| 325 out = open(out_path, 'w') | |
| 326 err = open(error_path, 'w') | |
| 327 rev_old = core.svn_opt_revision_t() | |
| 328 rev_old.kind = core.svn_opt_revision_number | |
| 329 rev_old.value.number = other_rev | |
| 330 rev_new = core.svn_opt_revision_t() | |
| 331 rev_new.kind = core.svn_opt_revision_number | |
| 332 rev_new.value.number = revision | |
| 333 client.diff3([], url2, rev_old, url, rev_new, True, True, | |
| 334 deleted, ignore_type, 'UTF-8', out, err, | |
| 335 self.client_context, self.pool) | |
| 336 out.close() | |
| 337 err.close() | |
| 338 assert len(open(error_path).read()) == 0 | |
| 339 diff = open(out_path).read() | |
| 340 os.chdir(old_cwd) | |
| 341 shutil.rmtree(tmpdir) | |
| 342 return diff | |
| 343 | |
| 344 def get_file(self, path, revision): | |
| 345 out = cStringIO.StringIO() | |
| 346 tmpdir = tempfile.mkdtemp('svnwrap_temp') | |
| 347 # hot tip: the swig bridge doesn't like StringIO for these bad boys | |
| 348 out_path = os.path.join(tmpdir, 'diffout') | |
| 349 out = open(out_path, 'w') | |
| 350 ra.get_file(self.ra, path,revision, out , None) | |
| 351 out.close() | |
| 352 x = open(out_path).read() | |
| 353 shutil.rmtree(tmpdir) | |
| 354 return x | |
| 355 | |
| 356 def proplist(self, path, revision, recurse=False): | |
| 357 rev = core.svn_opt_revision_t() | |
| 358 rev.kind = core.svn_opt_revision_number | |
| 359 rev.value.number = revision | |
| 360 if path[-1] == '/': | |
| 361 path = path[:-1] | |
| 362 if path[0] == '/': | |
| 363 path = path[1:] | |
| 364 pl = dict(client.proplist2(self.svn_url+'/'+path, rev, rev, True, | |
| 365 self.client_context, self.pool)) | |
| 366 pl2 = {} | |
| 367 for key, value in pl.iteritems(): | |
| 368 pl2[key[len(self.svn_url)+1:]] = value | |
| 369 return pl2 | |
| 370 | |
| 371 def fetch_all_files_to_dir(self, path, revision, checkout_path): | |
| 372 rev = core.svn_opt_revision_t() | |
| 373 rev.kind = core.svn_opt_revision_number | |
| 374 rev.value.number = revision | |
| 375 client.export3(self.svn_url+'/'+path, checkout_path, rev, | |
| 376 rev, True, True, True, 'LF', # should be 'CRLF' on win32 | |
| 377 self.client_context, self.pool) | |
| 378 | |
| 379 class SubversionRepoCanNotReplay(Exception): | |
| 380 """Exception raised when the svn server is too old to have replay. | |
| 381 """ |
