Mercurial > hgsubversion
comparison svnwrap/svn_swig_wrapper.py @ 316:c3c647aff97c
Merge with danchr's changes.
| author | Augie Fackler <durin42@gmail.com> |
|---|---|
| date | Sun, 03 May 2009 21:44:53 -0500 |
| parents | 97360cb777a8 |
| children | c4061e57974c |
comparison
equal
deleted
inserted
replaced
| 315:963d27a0b1c2 | 316:c3c647aff97c |
|---|---|
| 4 import shutil | 4 import shutil |
| 5 import sys | 5 import sys |
| 6 import tempfile | 6 import tempfile |
| 7 import urlparse | 7 import urlparse |
| 8 import urllib | 8 import urllib |
| 9 import hashlib | |
| 10 import collections | |
| 9 | 11 |
| 10 from svn import client | 12 from svn import client |
| 11 from svn import core | 13 from svn import core |
| 12 from svn import delta | 14 from svn import delta |
| 13 from svn import ra | 15 from svn import ra |
| 14 | 16 |
| 17 from mercurial import util as hgutil | |
| 18 | |
| 15 def version(): | 19 def version(): |
| 16 return '%d.%d.%d' % (core.SVN_VER_MAJOR, core.SVN_VER_MINOR, core.SVN_VER_MICRO) | 20 return '%d.%d.%d' % (core.SVN_VER_MAJOR, core.SVN_VER_MINOR, core.SVN_VER_MICRO) |
| 17 | 21 |
| 18 if (core.SVN_VER_MAJOR, core.SVN_VER_MINOR, core.SVN_VER_MICRO) < (1, 5, 0): #pragma: no cover | 22 if (core.SVN_VER_MAJOR, core.SVN_VER_MINOR, core.SVN_VER_MICRO) < (1, 5, 0): #pragma: no cover |
| 19 raise ImportError, 'You must have Subversion 1.5.0 or newer and matching SWIG bindings.' | 23 raise ImportError, 'You must have Subversion 1.5.0 or newer and matching SWIG bindings.' |
| 23 """ | 27 """ |
| 24 | 28 |
| 25 class SubversionRepoCanNotDiff(Exception): | 29 class SubversionRepoCanNotDiff(Exception): |
| 26 """Exception raised when the svn API diff3() command cannot be used | 30 """Exception raised when the svn API diff3() command cannot be used |
| 27 """ | 31 """ |
| 32 | |
| 33 '''Default chunk size used in fetch_history_at_paths() and revisions().''' | |
| 34 _chunk_size = 1000 | |
| 28 | 35 |
| 29 def optrev(revnum): | 36 def optrev(revnum): |
| 30 optrev = core.svn_opt_revision_t() | 37 optrev = core.svn_opt_revision_t() |
| 31 optrev.kind = core.svn_opt_revision_number | 38 optrev.kind = core.svn_opt_revision_number |
| 32 optrev.value.number = revnum | 39 optrev.value.number = revnum |
| 33 return optrev | 40 return optrev |
| 34 | 41 |
| 35 svn_config = core.svn_config_get_config(None) | 42 svn_config = core.svn_config_get_config(None) |
| 36 class RaCallbacks(ra.Callbacks): | 43 class RaCallbacks(ra.Callbacks): |
| 37 def open_tmp_file(self, pool): #pragma: no cover | 44 @staticmethod |
| 45 def open_tmp_file(pool): #pragma: no cover | |
| 38 (fd, fn) = tempfile.mkstemp() | 46 (fd, fn) = tempfile.mkstemp() |
| 39 os.close(fd) | 47 os.close(fd) |
| 40 return fn | 48 return fn |
| 41 | 49 |
| 42 def get_client_string(self, pool): | 50 @staticmethod |
| 51 def get_client_string(pool): | |
| 43 return 'hgsubversion' | 52 return 'hgsubversion' |
| 44 | 53 |
| 45 | |
| 46 def user_pass_prompt(realm, default_username, ms, pool): #pragma: no cover | 54 def user_pass_prompt(realm, default_username, ms, pool): #pragma: no cover |
| 55 # FIXME: should use getpass() and username() from mercurial.ui | |
| 47 creds = core.svn_auth_cred_simple_t() | 56 creds = core.svn_auth_cred_simple_t() |
| 48 creds.may_save = ms | 57 creds.may_save = ms |
| 49 if default_username: | 58 if default_username: |
| 50 sys.stderr.write('Auth realm: %s\n' % (realm,)) | 59 sys.stderr.write('Auth realm: %s\n' % (realm,)) |
| 51 creds.username = default_username | 60 creds.username = default_username |
| 115 else: | 124 else: |
| 116 user = urllib.unquote(userpass) or None | 125 user = urllib.unquote(userpass) or None |
| 117 url = urlparse.urlunparse((scheme, netloc, path, params, query, fragment)) | 126 url = urlparse.urlunparse((scheme, netloc, path, params, query, fragment)) |
| 118 return (user, passwd, url) | 127 return (user, passwd, url) |
| 119 | 128 |
| 120 class Revision(object): | 129 class Revision(tuple): |
| 121 """Wrapper for a Subversion revision. | 130 """Wrapper for a Subversion revision. |
| 131 | |
| 132 Derives from tuple in an attempt to minimise the memory footprint. | |
| 122 """ | 133 """ |
| 123 def __init__(self, revnum, author, message, date, paths, strip_path=''): | 134 def __new__(self, revnum, author, message, date, paths, strip_path=''): |
| 124 self.revnum, self.author, self.message = revnum, author, message | 135 _paths = {} |
| 125 self.date = date | |
| 126 self.paths = {} | |
| 127 if paths: | 136 if paths: |
| 128 for p in paths: | 137 for p in paths: |
| 129 self.paths[p[len(strip_path):]] = paths[p] | 138 _paths[p[len(strip_path):]] = paths[p] |
| 139 return tuple.__new__(self, | |
| 140 (revnum, author, message, date, _paths)) | |
| 141 | |
| 142 def get_revnum(self): | |
| 143 return self[0] | |
| 144 revnum = property(get_revnum) | |
| 145 | |
| 146 def get_author(self): | |
| 147 return self[1] | |
| 148 author = property(get_author) | |
| 149 | |
| 150 def get_message(self): | |
| 151 return self[2] | |
| 152 message = property(get_message) | |
| 153 | |
| 154 def get_date(self): | |
| 155 return self[3] | |
| 156 date = property(get_date) | |
| 157 | |
| 158 def get_paths(self): | |
| 159 return self[4] | |
| 160 paths = property(get_paths) | |
| 130 | 161 |
| 131 def __str__(self): | 162 def __str__(self): |
| 132 return 'r%d by %s' % (self.revnum, self.author) | 163 return 'r%d by %s' % (self.revnum, self.author) |
| 133 | 164 |
| 134 _svntypes = { | 165 _svntypes = { |
| 140 """Wrapper for a Subversion repository. | 171 """Wrapper for a Subversion repository. |
| 141 | 172 |
| 142 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. |
| 143 It takes a required param, the URL. | 174 It takes a required param, the URL. |
| 144 """ | 175 """ |
| 145 def __init__(self, url='', username='', password=''): | 176 def __init__(self, url='', username='', password='', head=None): |
| 146 parsed = parse_url(url) | 177 parsed = parse_url(url) |
| 147 # --username and --password override URL credentials | 178 # --username and --password override URL credentials |
| 148 self.username = username or parsed[0] | 179 self.username = username or parsed[0] |
| 149 self.password = password or parsed[1] | 180 self.password = password or parsed[1] |
| 150 self.svn_url = parsed[2] | 181 self.svn_url = parsed[2] |
| 151 self.auth_baton_pool = core.Pool() | 182 self.auth_baton_pool = core.Pool() |
| 152 self.auth_baton = _create_auth_baton(self.auth_baton_pool) | 183 self.auth_baton = _create_auth_baton(self.auth_baton_pool) |
| 184 # self.init_ra_and_client() assumes that a pool already exists | |
| 185 self.pool = core.Pool() | |
| 153 | 186 |
| 154 self.init_ra_and_client() | 187 self.init_ra_and_client() |
| 155 self.uuid = ra.get_uuid(self.ra, self.pool) | 188 self.uuid = ra.get_uuid(self.ra, self.pool) |
| 156 repo_root = ra.get_repos_root(self.ra, self.pool) | 189 repo_root = ra.get_repos_root(self.ra, self.pool) |
| 157 # *will* have a leading '/', would not if we used get_repos_root2 | 190 # *will* have a leading '/', would not if we used get_repos_root2 |
| 179 self.client_context.auth_baton = self.auth_baton | 212 self.client_context.auth_baton = self.auth_baton |
| 180 self.client_context.config = svn_config | 213 self.client_context.config = svn_config |
| 181 callbacks = RaCallbacks() | 214 callbacks = RaCallbacks() |
| 182 callbacks.auth_baton = self.auth_baton | 215 callbacks.auth_baton = self.auth_baton |
| 183 self.callbacks = callbacks | 216 self.callbacks = callbacks |
| 184 self.ra = ra.open2(self.svn_url.encode('utf-8'), callbacks, | 217 try: |
| 185 svn_config, self.pool) | 218 self.ra = ra.open2(self.svn_url.encode('utf-8'), callbacks, |
| 219 svn_config, self.pool) | |
| 220 except core.SubversionException, e: | |
| 221 raise hgutil.Abort(e.args[0]) | |
| 186 | 222 |
| 187 def HEAD(self): | 223 def HEAD(self): |
| 188 return ra.get_latest_revnum(self.ra, self.pool) | 224 return ra.get_latest_revnum(self.ra, self.pool) |
| 189 HEAD = property(HEAD) | 225 HEAD = property(HEAD) |
| 190 | 226 |
| 191 def START(self): | 227 def START(self): |
| 192 return 0 | 228 return 0 |
| 193 START = property(START) | 229 START = property(START) |
| 194 | 230 |
| 231 def last_changed_rev(self): | |
| 232 try: | |
| 233 holder = [] | |
| 234 ra.get_log(self.ra, [''], | |
| 235 self.HEAD, 1, | |
| 236 1, #limit of how many log messages to load | |
| 237 True, # don't need to know changed paths | |
| 238 True, # stop on copies | |
| 239 lambda paths, revnum, author, date, message, pool: | |
| 240 holder.append(revnum), | |
| 241 self.pool) | |
| 242 | |
| 243 return holder[-1] | |
| 244 except core.SubversionException, e: | |
| 245 if e.apr_err not in [core.SVN_ERR_FS_NOT_FOUND]: | |
| 246 raise | |
| 247 else: | |
| 248 return self.HEAD | |
| 249 last_changed_rev = property(last_changed_rev) | |
| 250 | |
| 195 def branches(self): | 251 def branches(self): |
| 196 """Get the branches defined in this repo assuming a standard layout. | 252 """Get the branches defined in this repo assuming a standard layout. |
| 253 | |
| 254 This method should be eliminated; this class does not have | |
| 255 sufficient knowledge to yield all known tags. | |
| 197 """ | 256 """ |
| 198 branches = self.list_dir('branches').keys() | 257 branches = self.list_dir('branches').keys() |
| 199 branch_info = {} | 258 branch_info = {} |
| 200 head=self.HEAD | 259 head=self.HEAD |
| 201 for b in branches: | 260 for b in branches: |
| 214 | 273 |
| 215 def tags(self): | 274 def tags(self): |
| 216 """Get the current tags in this repo assuming a standard layout. | 275 """Get the current tags in this repo assuming a standard layout. |
| 217 | 276 |
| 218 This returns a dictionary of tag: (source path, source rev) | 277 This returns a dictionary of tag: (source path, source rev) |
| 278 | |
| 279 This method should be eliminated; this class does not have | |
| 280 sufficient knowledge to yield all known tags. | |
| 219 """ | 281 """ |
| 220 return self.tags_at_rev(self.HEAD) | 282 return self.tags_at_rev(self.HEAD) |
| 221 tags = property(tags) | 283 tags = property(tags) |
| 222 | 284 |
| 223 def tags_at_rev(self, revision): | 285 def tags_at_rev(self, revision): |
| 286 """Get the tags in this repo at the given revision, assuming a | |
| 287 standard layout. | |
| 288 | |
| 289 This returns a dictionary of tag: (source path, source rev) | |
| 290 | |
| 291 This method should be eliminated; this class does not have | |
| 292 sufficient knowledge to yield all known tags. | |
| 293 """ | |
| 224 try: | 294 try: |
| 225 tags = self.list_dir('tags', revision=revision).keys() | 295 tags = self.list_dir('tags', revision=revision).keys() |
| 226 except core.SubversionException, e: | 296 except core.SubversionException, e: |
| 227 if e.apr_err == core.SVN_ERR_FS_NOT_FOUND: | 297 if e.apr_err == core.SVN_ERR_FS_NOT_FOUND: |
| 228 return {} | 298 return {} |
| 273 revision = self.HEAD | 343 revision = self.HEAD |
| 274 r = ra.get_dir2(self.ra, dir, revision, core.SVN_DIRENT_KIND, self.pool) | 344 r = ra.get_dir2(self.ra, dir, revision, core.SVN_DIRENT_KIND, self.pool) |
| 275 folders, props, junk = r | 345 folders, props, junk = r |
| 276 return folders | 346 return folders |
| 277 | 347 |
| 278 def revisions(self, start=None, chunk_size=1000): | 348 def revisions(self, start=None, stop=None, chunk_size=_chunk_size): |
| 279 """Load the history of this repo. | 349 """Load the history of this repo. |
| 280 | 350 |
| 281 This is LAZY. It returns a generator, and fetches a small number | 351 This is LAZY. It returns a generator, and fetches a small number |
| 282 of revisions at a time. | 352 of revisions at a time. |
| 283 | 353 |
| 286 """ | 356 """ |
| 287 return self.fetch_history_at_paths([''], start=start, | 357 return self.fetch_history_at_paths([''], start=start, |
| 288 chunk_size=chunk_size) | 358 chunk_size=chunk_size) |
| 289 | 359 |
| 290 def fetch_history_at_paths(self, paths, start=None, stop=None, | 360 def fetch_history_at_paths(self, paths, start=None, stop=None, |
| 291 chunk_size=1000): | 361 chunk_size=_chunk_size): |
| 292 revisions = [] | 362 '''TODO: This method should be merged with self.revisions() as |
| 293 def callback(paths, revnum, author, date, message, pool): | 363 they are now functionally equivalent.''' |
| 294 r = Revision(revnum, author, message, date, paths, | |
| 295 strip_path=self.subdir) | |
| 296 revisions.append(r) | |
| 297 if not start: | 364 if not start: |
| 298 start = self.START | 365 start = self.START |
| 299 if not stop: | 366 if not stop: |
| 300 stop = self.HEAD | 367 stop = self.HEAD |
| 301 while stop > start: | 368 while stop > start: |
| 302 ra.get_log(self.ra, | 369 def callback(paths, revnum, author, date, message, pool): |
| 303 paths, | 370 r = Revision(revnum, author, message, date, paths, |
| 304 start+1, | 371 strip_path=self.subdir) |
| 305 stop, | 372 revisions.append(r) |
| 306 chunk_size, #limit of how many log messages to load | 373 # use a queue; we only access revisions in a FIFO manner |
| 307 True, # don't need to know changed paths | 374 revisions = collections.deque() |
| 308 True, # stop on copies | 375 |
| 309 callback, | 376 try: |
| 310 self.pool) | 377 # TODO: using min(start + chunk_size, stop) may be preferable; |
| 311 if len(revisions) < chunk_size: | 378 # ra.get_log(), even with chunk_size set, takes a while |
| 312 # this means there was no history for the path, so force the | 379 # when converting the 65k+ rev. in LLVM. |
| 313 # loop to exit | 380 ra.get_log(self.ra, |
| 314 start = stop | 381 paths, |
| 382 start+1, | |
| 383 stop, | |
| 384 chunk_size, #limit of how many log messages to load | |
| 385 True, # don't need to know changed paths | |
| 386 True, # stop on copies | |
| 387 callback, | |
| 388 self.pool) | |
| 389 except core.SubversionException, e: | |
| 390 if e.apr_err not in [core.SVN_ERR_FS_NOT_FOUND]: | |
| 391 raise | |
| 392 else: | |
| 393 raise hgutil.Abort('%s not found at revision %d!' | |
| 394 % (self.subdir.rstrip('/'), stop)) | |
| 395 | |
| 396 while len(revisions) > 1: | |
| 397 yield revisions.popleft() | |
| 398 | |
| 399 if len(revisions) == 0: | |
| 400 # exit the loop; there is no history for the path. | |
| 401 break | |
| 315 else: | 402 else: |
| 316 start = revisions[-1].revnum | 403 r = revisions.popleft() |
| 317 while len(revisions) > 0: | 404 start = r.revnum |
| 318 yield revisions[0] | 405 yield r |
| 319 revisions.pop(0) | 406 self.init_ra_and_client() |
| 320 | 407 |
| 321 def commit(self, paths, message, file_data, base_revision, addeddirs, | 408 def commit(self, paths, message, file_data, base_revision, addeddirs, |
| 322 deleteddirs, properties, copies): | 409 deleteddirs, properties, copies): |
| 323 """Commits the appropriate targets from revision in editor's store. | 410 """Commits the appropriate targets from revision in editor's store. |
| 324 """ | 411 """ |
